Memory Full

La genèse du Starkos

Written by Targhan in March 2010

Initially written for Demoniak #8 and published on Push'n'Pop.

Mais qu'est-ce que cet article ? Et bien comme son nom l'indique à ceux qui savent ce que «genèse» signifie, je vais vous parler de la manière dont j'ai procédé pour achever Starkos. Je vais tenter de parler du projet de A à Z, par où j'ai commencé, quelles étaient les difficultés majeures, et là où j'ai fini (excellent, ce Château Compère). L'intérêt de cet article ? Et bien... Peut-être communiquer mon expérience, mes erreurs à ceux qui voudraient se frotter à des projets plutôt ambitieux. J'ai réussi à mener le Starkos à terme, c'est la preuve qu'on peut encore produire de gros programmes à notre époque.

Je vais essayer au départ de rester dans les généralités, mais je rentrerai dans les détails techniques très rapidement. Vous êtes là pour ça, non ? Donc si vous êtes espion industriel, ça vous évitera de cambrioler mon appart', y'a déjà assez de bordel comme ça. Je vous conseille également d'avoir des notions sur les trackers (patterns, instruments), ainsi que dans les caractéristiques du Starkos en lui-même.

Précisons que cet article n'est nullement là pour me vanter de quoi que ce soit je suis génial, il serait redondant de le prouver à nouveau... Je vais essayer d'être très critique. Déjà, Starkos, c'est un nom débile. 1ère erreur....

Le pourquoi de le comment
imageDéjà, pourquoi avoir décidé de faire un nouveau soundtracker ? Bien que peu exhaustifs, les logiciels de musique sur CPC sont de bonne facture (la suite vous le prouvera encore plus). L'AMC est puissant quoique plutôt inutilisable, Equinoxe était rigolo quand il est sorti, et le soundtracker de BSC est tout simplement excellent. Son support instrument est un peu limité en fait, mais à part ça, pas de problème particulier.

C'est en fait par curiosité, un jour d'une grisaille malsaine, que je me suis penché sur les trackers de cette belle machine qu'est l'Atari ST. On peut en trouver une bonne vingtaine, au bas mot. La plupart sont faits par des demomakers, certains sont commerciaux, il est d'ailleurs difficile, voire impossible de se les procurer (peut-être parce qu'ils ne sont jamais sortis). Bref, c'est avec les versions Bêta ou Demos que j'ai pu les décortiquer un peu.

Premier constat : ils sont horriblement buggés ou inconfortables. L'AMC est une merveille ergonomique comparé à certains d'entre eux. Quand le logiciel ne plante pas au bout de quelques clics, c'est que la souris n'est pas gérée, et que vous devez vous débrouiller avec des raccourcis claviers peu pratiques (mention spéciale à ce tracker en mode texte avec patterns et instruments sur un écran, affreux). Bref, ça sent les trucs destinés à une clientèle limitée (parfois même, à un groupe particulier).

Deuxième constat : ce ne sont pas des modèles de puissance. Finalement, et on peut féliciter BSC sur ce coup, peu de trackers surpassent le Soundtrakker 128. Bien sûr, il y a souvent des options spécifiques au ST, genre gestion des digidrums et sons SIDs pour les plus «récents». Si une grande partie des trackers ont un support pattern convenable (note+instrument+effet), ça pêche au niveau instrument. Certains logiciels ne permettent que 16 instruments (alors que la mémoire sur ST est tout de même de 512ko minimum !). Pire, ils sont basés sur des courbes ADSR, tout comme l'AMC, qui limitent les sons à des courbes de volumes conventionnelles. Encore pire, la gestion des arpeggios peut être limitée à un petit buffer bouclant sur 3 octets par instrument ! Plusieurs trackers suppriment d'ailleurs pratiquement la gestion du bruit, puisqu'offrant un support digidrum.... Bref, c'est pas terrible tout ça.

Par contre, là ou c'est intéressant, c'est au niveau des basses hard. La plupart ne gèrent qu'une seule onde par instrument, mais avec adaptation de fréquence hard par rapport à celle du son. De plus, il existe cet effet que j'ai baptisé «HardSync» qui synchronise les deux fréquences sonores.

Deux trackers m'ont marqué : le Chipmon2 de Scavenger, et le Megatizer. Les deux se joignent au niveau puissance du support instruments. Une liste de lignes permettent de définir pour chaque VBL les changements de volume, hard, frequence, etc... C'est en voyant ça que je me suis dit : «crévindieu, faudrait faire pareil (bien rouler les «r»)». Les possibilités sont assez faramineuses, en effet.

Pour des raisons que je trouve maintenant un peu bancales, j'ai choisi le modèle du Megatizer à propos du support Patterns : une note, un instrument, et un pitch. Pas d'effet. Je me disais que le support instrument permettrait de combler cette lacune, et me permettrait de gagner du temps. C'est peut-être le cas, mais ça rend l'utilisation des arpeggios un peu laborieuse, puisqu'on doit dupliquer un instrument si on désire changer l'arpeggio.

Pour contrecarrer l'absence de la colonne d'effet, j'ai dû imaginer cette Special Track, qui permet d'insérer les changements de vitesse (effet indispensable !) et déclencher les digidrums... Tout cela fut sympa à imaginer, mais a considérablement alourdi la programmation Pire, je dois coder Starkos 2.0 pour rattraper le coup !

Bref, c'est en voyant ces nouvelles possibilités que je me suis lancé sur Starkos. Après tout, le Soundtrakker 128 avait plus de 10 ans ! Il était temps de le remplacer par quelque chose de plus puissant.

Notons enfin que j'avais gardé le projet secret, à part pour une poignée de personnes, dont Orphee, Ramlaid et Grim. Je ne voulais pas qu'on me mette le couteau sous la gorge pour me motiver à finir... Finalement, ce fût un choix intelligent.

Au début du départ
Faire un projet de cette taille n'est pas aisé. Il y a beaucoup de détails à régler. Et puis je n'avais jamais travaillé sur un projet de cet envergure. Il valait mieux ne pas faire le malin et procéder étape par étape et avec méthode (au passage, je remercie mes profs de l'IUT. Enfin... pas tous : juste ceux qui servaient à quelque chose).

1ère question, la plus importante : Qu'est-ce que je veux faire ? Quelles seront les possibilités de mon logiciel ? (ça fait 2 questions, mais c'est pas grave).

Dès le départ, j'ai vu les choses en grand : je ne voulais plus limiter le musicien à 16 instruments et 44 patterns. J'en voulais autant que le CPC puisse en supporter ! Le modèle standard utilisé par les trackers CPC (et ST !! Ils sont tous très limités à ce sujet, ce qui m'étonne, vu la mémoire disponible) qui consiste à stocker en mémoire l'intégralité des patterns dans un format plus ou moins compacté et de taille fixe ne pouvait fonctionner ici. Examinons un format conventionnel de type BSC.

Sur un canal :

- Note (12 notes par octave, 8 octaves = de 0-95, codage sur 7 bits)
- Instrument (de 0 a &F;, codage sur 4 bits)
- Numéro effet (de 0 a &F;, codage sur 4 bits)
- Valeur effet (de 0 a &FF;, codage sur 8 bits)

En mixant l'instrument et le numéro d'effet en un octet, ça nous fait 3 octets pour une ligne. Une pattern en comporte 64 en général, et il y a 3 canaux : 3 octets*64 lignes*3 canaux = 576 octets. Ce n'est pas énorme à première vue. Le soundtrakker peut en stocker 44 en mémoire, en réservant une bank et une partie de la mémoire vive. Maintenant, notons qu'on ne parle que de 16 instruments. Je me suis dit : « autant en mettre 256, comme ça, tout le monde est content ». Tout de suite, il faut réserver un octet de plus dans le format !

Le support instrument du BSC est vraiment limité : un instrument est codé sur 8 octets pour son nom, plus 32 octets pour les deux courbes de volume et de bruit, plus 64 pour les variations de pitch. C'est dérisoire ! Tel que j'imagine mon support instrument, ceux-ci sont composés de lignes, qui elles-mêmes comportent un bon paquet de paramètres. Je choisis dès le départ un nombre maximum de lignes de 256, au moins personne ne sera limité !

En ce qui concerne les patterns, j'ai toujours trouvé idiot de stocker les 3 canaux entiers d'une pattern, alors que les dissocier permettraient d'une part de gagner de la mémoire, d'autre part de composer plus rapidement. Je décide donc qu'une Pattern sera décomposée en Tracks (le terme Position devient alors obsolète). Je ne connais d'ailleurs qu'un ou deux trackers qui envisagent le séquençage de cette manière. Il est amusant de constater que c'est plus de la fainéantise qu'autre chose que de ne pas utiliser cette technique : les patterns sont souvent constituées de 3 pointeurs qui pointent sur 3 tracks ! Le player du Sountrakker 128 peut 'potentiellement' supporter les tracks... Mais passons.

Puisque la musique est composée de Patterns, elles-mêmes composées de Tracks, je décide là encore de voir en grand : permettre 256 Positions. Une position pouvant adresser 3 tracks différentes, il serait stupide de limiter le tracker à 256 Tracks. J'en réserve donc 512. J'aurais pu en mettre 768 (256*3), mais je me suis dit : «tu n'as qu'une seule vie»... Problème réglé.

... alors voila. L'ambition, c'est bien, mais comment gérer tout ça ? Vous vous en doutez, si on a de telles prétentions, c'est soit qu'on a un QI de 32, soit un plan derrière la tête. J'avais heureusement une solution : réserver la mémoire de manière dynamique. C'est bien sûr plus compliqué à mettre en place, mais ça permet beaucoup de chose.

Cela entraine un autre problème : c'est bien gentil de n'allouer de la mémoire qu'aux blocs utilisés, mais ça ne change pas le fait qu'il est impossible de réserver plus de X patterns dans 128ko (X=mémoire totale dispo/taille d'une pattern) ! ! Et oui, donc, deuxième ruse : compacter les éléments non utilisés, et les décompacter lors de leur édition. Le CPC ne pourra pas contenir 512 patterns et 256 instruments remplis raz-la-goule, mais le pourra peut-être s'ils ne sont que partiellement remplis. C'est ce qui se produit généralement en musique. Flute, ça complique encore les choses ! Eh oui, mais foskifo. A noter qu'utiliser la mémoire dynamique permet également de créer des patterns de tailles arbitraires, ce qui facilite encore la composition. Mais nous aborderons la gestion mémoire plus loin (et si vous n'êtes plus là, et bien je le ferais tout seul, ça m'intéresse).

Une fois mes buts définis, il faut se poser la question suivante : Est-ce que je peux le faire ?

Cette question est bien entendue cruciale ! Et doublement critique : est-ce que je SAIS le faire, mais est-ce que je peux techniquement faire passer tout ça dans un CPC ? L'enthousiasme me fait répondre 'oui' à la 1ere question. Mais en y réfléchissant un peu plus, il peut y avoir problème.... Les nouveaux effets Hards, bien que finalement peu compliqués, sont nouveaux sur CPC (calcul de la fréquence des basses hards). J'ai donc entrepris de tester tout ça à l'aide d'un petit player extrêmement simple : je lui donne une note, il me calcule la basse hard et joue le tout. Second essai : on teste le Hardsync. Tout fonctionne. D'un point de vue PSG, il n'y a donc pas de difficulté.

La 2ème question.... L'idée de coder sur un vrai CPC un logiciel qui me semble alors de plus en plus complexe est rejetée très rapidement. Même en codant sur plusieurs sources assemblées séparément, ce serait un casse-tête abominable, vous en serez convaincu en lisant la suite, si vous ne l'êtes pas déjà. Je mets n'importe quel codeur au défi de faire un projet aussi gros avec Dams en moins de 10 ans et bien sûr, sans recoder l'intégralité du projet tous les ans ! Bien qu'ayant bénéficié d'une organisation relativement solide, je passais très régulièrement d'un module à un autre. J'ai donc sans honte décidé de développer sous Winape, sur PC... Histoire aussi de ne pas troubler mes habitudes, niark niark.

Préparation
Voila, je sais maintenant ce que je veux faire, et je sais même que je peux le faire ! Quel progrès.

Nouvelle étape : définir le désign du logiciel. Comme certains le savent déjà, l'interface est le plus gros travail dans un logiciel. Et il est faux de penser qu'elle n'aura aucune incidence sur le reste des routines. C'est peut-être un peu plus vrai sur PC, où l'on tend à séparer les «corps de métier», mais sur CPC, quand il faut obtenir des routines rapides, quand il faut ruser pour que tout fonctionne, c'est autre chose. J'ai donc pris ma plume, mon encre, et ai dépecé un bison facétieux pour faire de sa peau tannée le support idéal de mes élucubrations.

J'ai dessiné les différents écrans du logiciel. Sachant que les instruments seraient basés sur un système de ligne, j'ai su que je devais leur réserver une grande fenêtre. De même, les Patterns étant composées de 3 Tracks chacune + 1 Special Track + les 3 colonnes de transposition, ça demandait encore plus d'infos à afficher. Conclusion : il me faut trois écrans au minimum.

Une fois les écrans plus ou moins dessinés, je commence à designer les outils qui me seront nécessaires. Par outils, je parle de routines plus ou moins high level, comme la gestion du curseur, qui est très importante. De plus, j'élabore les interactions claviers : quels sont les raccourcis, comment je passe d'une page à une autre, etc... Bref, tout ça commence à avoir de la gueule.

imageEncore une énoooorme erreur (je préfère les noter en gros, histoire que ça serve de repère, le texte est assez dense) : j'avais sous-estimé la taille que prendraient le code et les buffers... J'ai décidé de faire le Starkos en Fullscreen. Hum. Cela dit, comment pouvais-je prévoir que le code me prendrait autant de place ?? De plus, j'avais commis une deuuuuxième erreur liée à la 1ère (donc ça fait qu'1 erreur et demi en tout) : j'avais l'idée saugrenue de me passer du système (cri d'effroi dans la salle). La plupart des demomakers savent tout gérer eux-même, cela dit c'est au niveau du lecteur de disquette que c'est plus embêtant. Ayant acquis suffisamment de connaissances sur le FDC à ce moment précis, je me suis dit que cela ne poserai pas de problème. Je suis donc parti sur cette lancée. Un peu plus tard survint le Bordelik Meeting 5. J'en profitais pour montrer ma preview à quelques quidams. Bien m'en a pris ! Shap m'a tout de suite conseillé de réutiliser le système, Offset de même dans ses mails. Le problème est le suivant : charger un fichier amsdos est simple, mais sauvegarder est relativement plus compliqué. Disons que manipuler le directory d'une disquette demande certaines précautions... En plus du risque (relatif, si on fait attention), cela entraûne des problèmes de compatibilité (argument ultime made in Offset) : et Parados ? Et Ana ? Je ne vais pas m'amuser à sortir une nouvelle version de Starkos dès qu'un mongol s'amusera à inventer un format (ça arrive heureusement peu souvent). J'ai donc accepté de garder le système, et de l'utiliser au moins pour les accès discs. Par la même occasion, j'ai enlevé le Fullscreen, n'ayant plus de mémoire à lui offrir.

Réalisation du player
Je pensais qu'il était temps d'avoir un peu de concret. Maintenant que j'y repense, ce ne fût pas tout à fait utile. Enthousiasmant, tout au plus.

Comme je pensais avoir cerné le support Pattern/Instrument, j'ai écrit le format du player sur papier. J'ai commencé par le format instrument pur et dur, car c'est je le pensais –la partie la plus complexe et longue à gérer. Les deux parties sont très optimisées, seules les données utiles sont codées. La partie son étant la mieux définie (le PSG restant égal à lui-même, à l'inverse du Starkos qui a un peu évolué/dévié en cours de route), mon format son ne changera qu'une fois (une feature en plus, rien de méchant). Tant mieux, la partie 'jouage' de son est assez complexe. Simple dans le principe, mais optimisée autant que j'ai pu. Il y a beaucoup de cas possibles. Avec ou sans son, volume, bruit, avec ou sans son hard, arpeggios ?, pitch ?, hardSync ? finetuning du hardsync ? Fréquences PSG/hard calculées ou forcées par l'utilisateur ? Bref, un joyeux bordel.

Détail cocasse : le player de son terminé, j'étais tout fier en constatant qu'il ne prenait, pour un canal, que 4 lignes au maximum dans les cas les plus complexes (sans envoyer les infos au PSG). Je me dis alors 4*3=12 lignes pour 3 voies ? Super ! La gestion ne devrait pas me prendre grand chose ! Allez hop, un player surpuissant à 25 lignes !! Résultat final, le player final en prend 35... Amusant de constater que la gestion d'une musique prend beaucoup plus que son «exécution».

Une fois le player fini, je me dit «J'ai fait le plus dur» ! Il n'y avait bien sûr rien de plus faux...

Gestion des données - Aperçu
imageCette phase est l'une des plus importantes. Heureusement, je me suis pas mal débrouillé pour une fois, ce qui fait que je n'ai pas eu à recoder mes routines low-level. Le seul vrai problème que je n'ai pas pu surmonter est le défilement des patterns pendant la lecture de la musique, vous allez savoir pourquoi.

J'ai donc dit que les Tracks et Instruments sont gérés de manière dynamique. On ne les crée que s'ils sont utilisés. En pratique, ça ne s'est pas produit comme ça, pour des raisons de simplicité. En fait tous ces éléments existent au lancement de Starkos. C'est juste qu'ils ont une taille minimale (c'est la raison principale pour laquelle je n'ai autorisé que 512 tracks et non pas 768, je gagne un peu en mémoire). Pour retrouver facilement mes éléments dans la mémoire, j'ai choisi de créer trois tables d'index. Il existe donc une table d'index pour les instruments, les tracks et les special tracks. Les éléments de ces tables sont des pointeurs 16 bits sur le début de chaque élément. Comme ils sont tous créés à la base, il n'existe jamais de pointeur nul. Comme me le suggéra Zik un peu plus tard, j'aurais pu éliminer ces tables en créant des listes chaînées : au début de chaque élément, se trouverait l'adresse relative 16 bits vers l'élément suivant. C'est assez sympathique, mais dans la pratique ça m'aurait posé plus de problèmes. Pour retrouver le Xième élément, vous devez faire x lectures et additions dans la mémoire. Je perdrais donc en rapidité (le player à 300hz n'aurait pas apprécié). Mais surtout, j'avais déjà largement commencé mes routines !

Pour gérer la mémoire de manière efficace, je choisis que chaque élément commence par un mot donnant sa taille. Quel intérêt, vous direz-vous, alors que l'on peut la calculer grâce à la table d'index (taille élément x = (adresse élément x+1) - (adresse élément x)) ? Réponse : pour ne pas avoir à réorganiser ma mémoire à chaque opération.

Et oui, c'est bien gentil de réserver de la mémoire (plus ou moins) dynamiquement, mais que se passe-t-il lorsqu'un élément grandit ? Et bien il faut faire de la place, pardi ! On pousse donc les blocs supérieurs à l'aide d'un gros LDDR bien lourdaud. On mets à jour notre élément, et dernière étape, on doit aussi modifier notre table d'index ! L'adresse de l'élément modifié n'a pas bougé, mais on a déplacé les éléments qui se trouvaient derrière ! Il ne s'agit heureusement que de simples additions, rien de méchant. Et elles ne sont à effectuer que lorsque la mémoire doit être réorganisée.

Mais tout ça ne répond pas à la question : à quoi sert la taille puisqu'on peut encore la calculer grâce à la table d'index ? Facile : lorsqu'un élément modifié devient plus petit que précédemment ! On écrase l'ancien élément, inutile d'éliminer les octets de trop. On gagne ainsi un précieux temps-machine, pas besoin de modifier l'index, pas de LDIR à faire. C'est le mode 'Optimisation Normale' du Starkos. Dans le mode 'Maximum', je réorganise toujours. Dans le cas d'une musique gigantesque, ça peut-être utile (mais à mon avis, aucun humain ne l'utilisera. Madram, un jour, peut-être).

L'option Optimise dans le menu MISC permets d'optimiser la mémoire (et non pas de formater la disquette, le titre est trompeur). Je compare les adresses par rapport à la taille des éléments, et remarque tout de suite s'il existe des octets inutilisés entre eux. Si c'est la cas, hop, LDIR et modification de l'index.

Autre point à noter : pour optimiser encore le tout, j'ai décidé d'étaler les éléments à chaque 'bout' de la mémoire, et dans des sens opposés. Ainsi, les Instruments se trouvent dans le bas de la mémoire, et sont empilés. Les Tracks sont placées en haut de la mémoire, dans l'ordre descendant. Ça m'a empêché d'utiliser les mêmes routines de gestion mémoire, mais je gagne beaucoup en temps machine. Par exemple, si j'avais empilé tous les éléments dans le même sens (Instruments puis Tracks), modifier le 1er instrument aurait eu des conséquences également sur les adresses des Tracks ! Pour info, j'ai placé les Specials Tracks avant les Instruments. Comme elles sont peu utilisées, la perte n'est que minime.

Voila en gros comment je gère mes éléments.

Utilisation des données
Giga important. C'est bien beau d'avoir des données compactées, mais comment les réutiliser dans un utilitaire ? Prenons l'exemple d'une édition de pattern. Vous baladez votre curseur et entrez des notes. Cela signifie-t-il que pour chaque modification, on doit choper l'adresse de la pattern dans la table d'index, compacter la nouvelle pattern, et dans le pire des cas, réorganiser la mémoire avant de la ranger ?? Il est 'possible' de procéder ainsi, mais ça va ramer comme Mad (...parce que Mad rame. Oula, blague minable). Il faut donc trouver autre chose.

Le principe que j'ai utilisé est classique et le codeur qui sommeille en vous (il serait peut-être temps qu'il se mette au boulot d'ailleurs) aura tôt fait de l'entrevoir : il suffit de travailler sur des éléments décompactés. Lorsque j'édite une pattern, je décompacte les 3 tracks qui la composent (j'obtiens un format à la BSC). L'utilisateur ajoute ses données qui sont stockées de manière brutes dans ces buffers. Lorsqu'il a fini son travail (lorsqu'il change de page ou passe à une autre pattern), je compacte les tracks et les place en mémoire, la réorganise si nécessaire. Même principe pour les Instruments et les Specials Tracks.

Par soucis d'optimisation, un flag m'indique si oui ou non une modification a bien été effectuée. De plus, on remarque qu'un utilisateur ne peut pas travailler à la fois sur un Instrument et sur une Pattern. On peut donc se permettre de confondre leur buffer.

Enfin, je copie systématiquement un élément en RAM centrale avant de le décompacter/compacter. Au niveau décompactage, ça n'a qu'un intérêt limité (gain minime de temps machine, un peu plus facile à programmer). Mais en écriture, c'est plus problématique, je ne peux pas l'écrire directement en bank, car je ne connais pas la nouvelle taille de l'instrument, et peut-être que ça ne passera pas à la place de l'ancienne version ! Il est donc nécessaire de passer par ce buffer intermédiaire.

Dernière remarque, le genre de détail auquel il faut faire très attention, il m'est impossible de pointer directement vers les 3 buffers décompactés. Pourquoi ? Parce que que se passerait-il si la pattern actuelle utilisait 2 ou 3 fois la même Track ? Seule la dernière Track stockée conserverait ses informations. Donc, avant de décompacter les 3 Tracks, je vérifie si ce sont les mêmes, et ne réserve que 1, 2 ou 3 buffers selon que les Tracks sont différentes ou non. De plus, c'est cette routine qui va mettre à jour les pointeurs vers ces buffers. Mes routines d'édition ne se soucient pas de ce problème, c'est transparent pour elles. Elles ont juste à utiliser le pointeur de buffer relatif au canal qu'elles éditent et à travailler de manière relative à lui. On s'aperçoit que ça marche très bien : utilisez la même Track sur les 3 canaux sonores et entrez une note : elle sera reportée sur les 3 colonnes !

Organisation mémoire
imageTrès très important. Là aussi, je me suis pas mal débrouillé. Je commence à placer ce qui me semble «figé». Je place la mémoire écran tout en haut de la RAM centrale, soit à partir de #c000, sauf au départ lorsque j'utilisais le mode fullscreen (#8000-#ffff). Je place mes fontes le plus haut possible sous le système (sous #a500).

Par soucis de simplicité, je place mon code en bas de la mémoire centrale, en #100 (comme d'habitude en ce qui me concerne). Ma pile, en #100 également (n'oublions pas qu'un PUSH décrémente SP d'abord, donc aucun risque). Je ne savais pas encore que le code allait être aussi énorme, mais je me suis tout de même aperçu qu'il n'était pas sage de placer les éléments de la musique en RAM centrale. Je finis donc de combler celle-ci : j'y place mes buffers de décompactage et les buffers de copies (Nombreux : copie de 1 track, de 3 tracks, d'une special track, et 3 buffers pour les copies 'pas à pas' de lignes de Tracks, Special Tracks ou d'Instruments. Ca pèse lourd, ils ne peuvent pas être confondus). Pour Starkos 1.0, il ne me reste a peine que 2ko de libre dans la ram centrale ! Le code fait...32 ko. Oui, c'est gros.

Les banks sont occupées par les données musiques. Cela pose plusieurs problèmes. Déjà, travailler avec les banks de manière paginée (entre #4000-#7fff) est très difficilement gérable. Que se passe-t-il si un instrument chevauche deux banks ? Trop lent à gérer ! Empêcher les chevauchements ? Gonflant à gérer et pertes de mémoire puisqu'on laissera alors du vide à la fin de chaque bank. Bref, il n'existe qu'une véritable solution : le switching des 4 pages. En activant la bank #c2, les 4 banks prennent place en mémoire centrale. On peut ainsi accéder à 64ko de mémoire linéairement ! Cela pose problème : il faut se réserver une zone de code qui gèrera les transitions. Le Z80 n'a aucun mal à passer d'un 'coté' à l'autre. Pour un être humain, la logique est moins évidente. Si vous passez en #c2 par mégarde, le Z80 y lira les octets perdus qui s'y trouvent. Joli plantage !

De plus, le switching cela n'est pas très System-friendly. Nous verrons par la suite que ça m'a un peu gêné, même si je ne me sers du système qu'occasionnellement. Attention également à la pile ! Il faut juste faire attention que, lors du retour en RAM 'normale', la pile se trouve au même endroit que lorsque le switching s'est produit (en codant «normalement» cela ne pose aucune contrainte finalement). Elle retrouve ainsi ses données et votre code retombe sur ses pattes en douceur.

Autre problème, les interruptions ! Si vous ne les coupez pas, il faut placer un petit quelque chose en #4038 de la bank #c4 (soit, en #38 en switchant #c2) afin de gérer leur comportement !

Bref, rien de catastrophique à gérer, mais cela nécessite un minimum de rigueur.

Le début de #c2 contient donc la gestion des interruptions en #38, la pile, ainsi que du code de liaison entre les deux «faces» de la mémoire, pour permettre les transferts de données notamment. Tout cela est suivi par le player. Il était beaucoup plus judicieux de le placer ici, puisqu'il est directement lié aux données compactées. Le player est suivi des 3 tables d'index (Special tracks, Tracks, Instruments), elles-mêmes suivies des données des Special tracks et Instruments. S'ensuit un gros vide (qui sera comblé par les données lorsque leur volume augmentera). A la fin de la mémoire, gisent les Tracks.

Voilà voilà! C'est ma foi un peu plus compliqué que les trackers habituels (c'est à dire, tous), mais au moins personne ira me dire qu'il n'a pas assez de mémoire libre pour sa musique.

Low level
Pour commencer le low-level (c'est-à-dire, les routines les plus proches du hardware), il faut tracer les grandes lignes hardwares d'abord :
- Les interruptions sont activées. Je compte y placer les tests claviers ainsi que le player. Comme il peut monter à 300hz, une petite routine placée sur chaque interruption décidera s'il faut l'appeler ou non.
- Le système ne sera utilisé que pour les accès-disque. On pensera à repositionner la pile, les registres auxiliaires dont il a besoin, son petit code en #38 et à bien agiter avant chaque utilisation.
- Les couleurs ne changent pas, mais on les définira à l'aide des vecteurs systèmes. Sinon, lors d'un accès disque, nos couleurs hard seront remplacées par celle du système.

Comme je l'ai dis plus haut, je comptais me passer du système au départ. J'ai donc recréé la gestion du clavier. Bien entendu, c'est un peu plus compliqué que la routine de base dans une démo. Ici, il faut gérer la répétition des touches, tester TOUTES les touches, ajouter des flags si Shift/Control sont appuyés ou non. J'ai été un peu surpris par la relative complexité de la chose. Après le player, c'est la 2ème routine créée.

Ensuite, pour bien cerner le logiciel et surtout pour ne pas se planter, j'ai écris sur papier l'entête des principales routines de traitement des données, avec leur fonction et les conditions d'entrées/sorties. Voici un petit extrait (je ne vous décris pas la nomenclature des buffers utilisés ; si vous avez suivi c'est facile de comprendre) :

GetAdrInstr = Donne l'adresse en bank d'un instrument
- Entrée : A = no instrument
- Sortie : HL = adresse en bank (linéaire)

GetInstrName = Chope le nom d'un instrument et rempli un buffer de 8 octets
- Entrée : HL = buffer A = no instrument
- Sortie : buffer rempli

GetInstrCpt = Copie un instrument compacté de banks vers bufferCPT
- Entrée : A = no instrument à lire

CptInstr = Compacte un instrument de bufferDEC vers bufferCPT. Compacte uniquement la partie jouable de l'instrument.

DecInstr = Decompacte un instrument cpt du bufferCPT vers bufferDEC

PutInstr = Copie instrument cpt de bufferCPT vers bank.
- Entrée : A = no instrument destination

IsInstrEmpty = Regarde si un instrument est vide ou non
- Entrée : A = no instrument
- Sortie : Carry = 1 = vide

GetInstrSize = Trouve la taille d'un instrument compacte en bank.
- Entrée : A = no instrument
- Sortie : HL = taille instr (>=2)

CalcInstrSpace = Calcule la taille UTILISEE par instrument en bank. Quelle différence avec GetInstrSize? Et bien cette fois on calcule la taille non pas en regardant le header de l'instrument, mais en soustrayant le début de l'instrument précédent (+haut en mém) et le début de l'instrument actuel. On obtient la taille qui lui est allouée, qui est >= taille de l'instrument.
- Entrée : A = no instrument
- Sortie : HL = taille instr (>=2)

...


La liste n'est pas exhaustive.

La gestion des instruments s'est assez bien passée et je ne regrette rien. Celle des patterns est par contre beaucoup plus complexe. Son low-level est bien fait, mais j'ai eu pas mal d'embrouille à cause du player (cf plus loin).

(Ici) Le player
Parlons un peu de la manière dont il est géré. En fait, je l'appelle de deux manières. Soit j'ai appuyé sur «Play Pattern/Song», soit j'ai entré une note au clavier (ou utilisé la touche COPY dans une pattern (qui lit une ligne et joue les 3 voies)). Pourquoi avoir fait une différence ? Par soucis d'optimisation. Le «jouage» d'un son du clavier est effectué (tout comme COPY dans une pattern ) à partir du player : j'envoie des infos au player son qui va me les jouer. Puisqu'il n'y a pas de pattern a gérer, ça me permet de gagner du temps machine et donc de gérer tranquillement le reste du logiciel, même avec une musique à 300hz. Par contre, quand j'appuie sur Play, les choses se gâtent car la lecture/gestion des patterns prend pas mal de temps machine. Je coupe donc les interruptions, joue la musique à chaque Halt, gère moi-même le test de touche en hard (simple test de la touche Esc), et réactualise les affichages (en haut de l'écran : position, pattern et vitesse actuelle), un par un.

Maintenant que j'y pense, pourquoi ne pas avoir tout simplement mis le player sous interruption et le reste en «normal» ? L'affichage aurait été interrompu par le player, je ne vois pas où est le problème... A la limite, garder une routine rapide pour le clavier à ce moment là, mais c'est tout... Encore un mystère insondable, à ranger près de l'Atlantide, l'ile de Pâques, le monstre du Loch Ness, et les vidéos de Plissken.

Personnellement, je ne suis pas du tout satisfait de la manière dont j'ai géré ce player. 1ère grosse erreur (qui a entraûné le reste) : j'ai voulu réutiliser le player que j'avais déjà fait. Grosse gaffe. Pourquoi ? Parcequ'un player «terminé» est structurellement figé, est fait pour n'exécuter qu'une tâche : jouer la musique entière. C'était un player pour une démo, pas pour un utilitaire ! Il est difficile de ne lui faire jouer qu'un seul son, ou une partie de la musique. Bref, j'aurais dû en coder un spécial pour l'utilitaire et non jouer au docteur avec celui que j'avais (je dirais plutôt chirurgien en fait). Si je code un Starkos 2.0, le player sera continuellement sous interruption, continuellement en train de jouer 3 sons vides. Et je lui balancerai des commandes genre 'note : C#5, volume à 15, voie 1', ou encore 'volume à 3, voie 2'. Bref, ce sera beaucoup plus «ouvert».

Enfin, une question qui hante certains musiciens (et certains codeurs qui devraient penser à autre chose) : pourquoi ne pas avoir fait le défilement de pattern dans la version 1.0 ? Et bien voila pourquoi. Le player lit des données compactées. L'afficheur de pattern lis des données décompactées. Il n'existe pas de lien direct entre eux. Solution : il aurait fallu décompacter une pattern au début de chaque position et lancer la routine d'affichage. Mais ma routine de décompactage n'est pas faite pour cohabiter avec le player (je stoppe le player pour switcher vers #c2), et n'est pas spécialement rapide... Il aurait fallu une routine spécialisée et je ne me sentais pas l'envie de le faire, il me restait tant de truc a faire et j'avais si peu de mémoire libre... Mais le défilement de pattern est surtout quelque chose que je pensais facile à faire, je n'y ai donc pas trop pensé dès le début. Je me suis mal organisé à ce niveau. Bah, tout a été résolu dans la version suivante...

Autre truc à coder, très important : des outils de conversion ! Convertir un nombre en mémoire vers de l'ASCII, en prenant en compte son signe et un nombre précis de chiffre... L'inverse est également à faire (galère). Bref, voila le genre de routines qui gavent mais bon, une fois que c'est fait... En plus, c'est réutilisable à mort.

High level
Le high-level concerne l'interface, en gros. C'est un travail plutôt énorme, assez fastidieux et plus complexe que je ne l'aurais cru. Un bon nombre de problèmes vient de là.

1ère routine codée : le writter ! Bon, pas grand chose à dire, si ce n'est que les routines sont spécialisées en fonction de l'affichage (fonte 8*8 ou 4*8), et que la 1ère est très rapide. Notons aussi qu'elle permet, à l'aide d'un petit AND, de changer la couleur de la police à volonté. Une couche supérieure englobe le writter et lui permet de lire des nombres hexa 4/8/12/16 bits, signés ou non. Elle permet aussi d'afficher une liste de données (texte et nombres) à des emplacements précis. Permet d'écrire plein de trucs avec juste deux lignes de code.

2ème routine, l'afficheur de cadre ! Je vais pas balancer, mais certains se permettent de sauver des pages écrans complètes en guise d'interface... Pas bien BSC (oups). La présentation du Starkos étant différente à chaque page, il était nécessaire d'avoir une routine de ce genre. La encore, une liste de données est utilisée. Le format est sympathique et permet moultes choses :

defw 'pos Y' (#ffff = fin liste)
defb 'type de boite' (%abcd000e e = boite vide(0) ou pleine (1) a = bord haut b = bas c = gauche d = droite 0 = affiche bord)
defb 'pos X', 'hauteur'
defb 'largeur en octet'
defb 'espacement bord gauche en pixel'
defb 'espacement bord droit en pixel'


Bref, le genre de routine sympa à coder (mais j'avoue avoir eu du mal) et cool à utiliser.

3ème routine, d'une importance capitale, et dont je ne suis pas satisfait : la gestion du curseur ! Mine de rien, c'est énorme. Car il ne s'agit pas juste du curseur dans un menu, c'est beaucoup plus large. Pouvoir se positionner sur un nombre, utiliser les flèches avec Shift et/ou Control et le voir s'incrémenter, c'est cette routine aussi ! A la base, je me suis bien débrouillé. Une liste définit la position de tous les éléments accessibles par le curseur, le type de la donnée, ses caractéristiques, mais aussi où aller quand on appuie sur les touches du curseur. En gros, pour utiliser un curseur dans un page, j'ai deux lignes de code ! Pointer IX sur ma liste, et appeler la routine GERECURS. C'est très puissant et bien pratique. En plus de tout ça, il me fallait aussi créer une routine genre Input en basic qui me permettrait de lire les entrées au clavier de l'utilisateur, selon les caractéristiques bien précises du nombre actuel (genre : signé, 3 chiffres. Ou juste du texte). Double galère à coder...

Voici la gueule du format du curseur :

- defb Type de données, X, Y, taille curseur en caractères
- defb a,b,c,d = où aller quand on appuie sur gauche/droite/haut/bas (saute sur la Xième entrée de la liste. 255 = choix impossible)
- defw pointeur sur nombre ou texte


Si la donnée est numérique :

- defb/w minimum, maximum
- defb/w x,y,z = valeurs à ajouter ou décrémenter quand on utilise Shift (x), Control (y) ou les deux (z).


Sympa, non ? Quand j'ai commencé le Starkos j'ai pensé que ce serait suffisant, malheureusement il manquait un petit (gros) truc : les interactions. Genre : que se passe-t-il si la modification de la valeur entraûne un changement sur d'autres valeurs et qu'il faille les afficher aussi ? Regardez la page des instruments. Switchez quelques flags dans les colonnes. Certains paramètres se transforment en '---' ou au contraire, s'activent. Pour arriver à ce résultat, j'ai dû ruser. Vous vous dites que j'aurais pu rajouter des options dans mon format, mais non, je l'ai pas fait, et je me demande pourquoi !!

Pour corriger ce problème, j'ai ajouté du code à l'endroit où la valeur actuelle est réaffichée après modification. J'ai ajouté quelques tests, genre : Si je me trouve dans la page Instrument, que je suis sur la colonne 'Hard', et que la valeur est de 0, alors active la colonne 'Volume', et réaffiche moi tout ça ! Bref, ce n'est que du bidouillage, et ça s'est empiré, puisque de nombreux cas se sont présentés. Tout ça dans une routine appelée 'AffChiff' censée uniquement réafficher le nombre actuel... Super nase !

Bref, ce high level est assez puissant, mais je me suis lancé un peu trop rapidement dedans, et ça m'a valu de patauger dans du code pas super présentable et maintenable...

Accès-Diks
Petit paragraphe sur les accès diks. Comme je m'impose des musiques pouvant atteindre 54ko, qu'elles sont stockées en bank, que le système est incapable de gérer des fichiers binaires dans ces conditions, j'ai dû utiliser des fichiers ASCII. Mis à part le chargement d'une certaine lenteur, ça marche plutôt bien.

Bon, j'avoue, ça n'a pas toujours été le cas ! Comme je l'avais dit, c'est pendant les accès-disk que j'autorise le système à cohabiter. Ben c'est bien lourdingue ! Prenons exemple sur la lecture d'une musique. Je lis le fichier ASCII linéairement (logique, et en plus j'ai pas le choix). Le format me donne les infos du chaûnage que je lis d'une traite, combien il y a d'instruments, de tracks, de special tracks. A chaque fois que je lis un élément, je stoppe le système et copie l'élément en question dans les banks. Puis je réactive le système et continue. Je ne teste même pas la fin du fichier en fait. Puisque je sais combien d'élément il me reste à lire, j'utilise un compteur et ferme le fichier quand je pense avoir tout lu. Et voila ! J'ai eu pas mal de problèmes car le système a bien entendu besoin de ses registres, sa pile, ses interruptions... Même en faisant attention, j'ai eu un bon paquet de plantages.

A propos des fichiers ASCII...
En ce qui concerne le Soundtrakker 128, BSC aurait très bien pu permettre d'utiliser environ 129 patterns, puisqu'il n'utilise qu'une seule bank et que les 3 autres ne sont pas utilisées... Pourquoi ne l'a-t-il pas fait ? Et bien probablement parce qu'il ne voulait pas utiliser de fichiers ASCII ! Il réserve la bank #c4 et une partie de la mémoire vive (avant #4000) à sa musique. Ainsi, lors de la sauvegarde, il a juste à ouvrir la bank et à sauver le tout. C'est un peu dommage d'avoir limité le logiciel pour un problème technique facilement contournable ! Mais bon...

Pour les intéressés, il existe la méthode à Antoine. Son fameux logiciel Super Monitor permet la sauvegarde de fichier de tout type (oui oui), de n'importe quel endroit de la mémoire, de n'importe quelle taille, quelle que soit la bank ouverte (ce qui inclut #c2 !). Bref, le gars a trouvé une technique très intéressante. J'y ai regardé de plus près, mais ai abandonné en cours de route, c'est vraiment le bordel. En gros, il utilise le vecteur système #bc8c pour ouvrir son fichier en sortie, normal. La où ça devient dingue, c'est qu'il créé lui même le header amsdos de son fichier - ce qui inclue le calcul du checksum - en fonction de son type. En gros, il a recréé (en partie) la fonction #bc95 (écriture d'un caractère en sortie) et gère lui-même son buffer. Ingénieux ! Je n'ai pas cherché à adopter sa technique, car il tape un peu partout dans le système, c'est un peu crade et j'avais autre chose à faire. Et puis la solution avec un fichier ASCII fonctionne, donc... Mais s'il y a des amateurs qui s'intéressent à sa technique, je serais ravi d'entendre la conclusion de leurs efforts.

J'ouvre un paragraphe concernant un détail, mais qui a toute son importance !! Question : comment faire pour trouver le nombre d'octets restants sur un disk ? L'amsdos donne la liste des fichiers VISIBLES sur le disk, ainsi que leur taille, mais c'est tout. Il suffirait alors de soustraire à 178 la somme des tailles trouvées dans le buffer généré par l'amsdos. C'est la méthode utilisée par le Sountrakker. MAIS : par 'visible', il faut comprendre : fichiers non cachés, user 0 ! Voila pourquoi on peut tomber sur des 'Disc full' alors que le Sountrakker affiche '100ko free' ! Pire, si vous disposez de Parados, ce calcul tombe à l'eau car vous disposez alors de plus de 178ko ! Mais que faire ? Eheh, le père Ghan a heureusement trouvé une technique top rulez (sauf qu'Offset a trouvé une faille : ça marche pas sous Rodos. Entre nous, tout le monde s'en tape). Lorsque l'on appelle le vecteur #bc9b, non seulement un buffer est généré, mais en plus le catalogue s'affiche à l'écran. Le nombre de kilos restants est fiable, et c'est ça que nous devons récupérer. Une technique qui marche, mais peu élégante, consisterait à bypasser le vecteur #bb5a et de stocker tout ce qu'il reçoit dans un buffer. A la fin du catalogue, faire une recherche sur 'K free', et choper les 3 octets précédents ! Ma technique est plus économique : je me réserve un buffer de 13 octets et bypasse le vecteur #bb5a en y inscrivant un petit bout de code qui lui demande de décaler le contenu de ce petit buffer «vers la gauche» (LDIR), ce qui va détruire le 1er octet du buffer. J'inscris à la fin de ce buffer le contenu de A, c'est à dire l'octet envoyé à #bb5a. Lorsque le vecteur catalogue a fini sa besogne, les 3 1ers octets du buffer correspondent à la séquence ASCII du nombre de ko libres ! Et voila :). Pourquoi notre buffer fait 13 octets, me direz vous ? Car à la fin du catalogue, l'amsdos saute 2 lignes (#d #a). Nous avons donc : 'XXXK free' suivi de #d,#a,#d,#a. 13 octets pile poil. Voila une technique qui pourra vous être utile...

Et quelque temps après, Zik me confia qu'il avait trouvé un moyen tout simple pour faire la même chose... Le vecteur qui rend le catalogue... rend également le nombre de ko qui reste... dans l'un des registres !! C'était pas documenté !! Grrr !!!

imageGénérateurs
Ceux-là ont été bien gonflants à faire... Le générateur de player, ça a été assez facile, j'ai assemblé deux players à deux endroits différents en mémoire et un petit utilitaire m'a crée une liste de DEFW pointant sur les adresses relatives au début du player qui changent. Il ne reste ensuite plus qu'a additionner l'adresse relative de relocation, et voila, le player est relogé.

Pour compiler la musique, ce fût autre chose... Comme je le dis dans la notice, la musique est entièrement nettoyée de tout instrument/pattern/special track non utilisé. La taille maximum des pattern est testée, pour qu'aucune information inutile ne soit stockée. Enfin, j'avais trouvé encore quelques optimisations au niveau du compactage des patterns... Bref, ça peut sembler simple à faire, mais j'ai franchement galéré. Au moins, une démo, quand ça plante, on sait plus ou moins pourquoi. Avec mon format changé, je devais non seulement modifier mon compacteur, mais aussi le player ! Pas facile. En plus, je me suis empêtrer à vouloir optimiser autant que je pouvais les listes de tracks, specials tracks et transpositions... Pleins de tests partout, du compactage de séquence au RLE (technique différente pour chaque liste)... Bref, pas super intéressant à coder, difficile de se rendre compte si ça marche bien, player qui perd en temps machine ce que la musique gagne en mémoire... Compilation de la musique en 2 passes (comme un assembleur)... Purée, j'ai pas choisi le chemin le plus facile.

Mais bon, ça marche alors je suis content.

Conclusion
Bon, et bien cet article touche à sa fin. J'espère qu'il vous aura intéressé, que vous aurez appris quelques trucs. Comme vous le voyez, il suffit la plupart du temps d'être bien organisé pour arriver à ses fins. La motivation n'est pas à oublier non plus...

Bref, du haut de ses 23855 lignes de code (uniquement le Starkos !), ça représente beaucoup de boulot, mais maintenant je sais que j'aurais pu optimiser par-ci par-là et que j'aurais pu le sortir encore plus vite si j'avais mieux réfléchi au problème.

Ce serait mon conseil à tous ceux qui veulent sortir des trucs un peu ambitieux : réfléchissez bien à ce que vous voulez faire. De plus, commentez au maximum vos sources ! Décrivez bien ce que fait chaque routine, entrées/sorties, ses particularités.

Voilà voilà. En ce qui me concerne, le Starkos est un projet qui m'a passionné, faire les générateurs àla fin était par contre très gavant... Mes seuls regrets viennent du fait des quelques faiblesses du logiciel que j'aurais pu corriger...

Mais bon, là où il y a des problèmes, il y a des solutions... A suivre :)

Targhan

PS : support musical de prédilection lors de l'elaboration du Starkos : Aenima de Tool, Dusk and her Embrace/Cruelty and the Beast de Cradle of Filth ! Je les conseille à tous...