Memory Full

Sampling et digitracking

Written by Targhan in April 2017

Bonjour à tous ! Bienvenue dans Demoni... Ah excusez-moi, on me signale un changement de situation. Grande première mondiale, voici un de mes articles dans Another World !

Le sieur X m'a demandé de parler de samples. Mais voyons plus large, je parlerai aussi Digitracking (l'art de jouer des samples sur plusieurs canaux).

Je commencerai par mettre les points sur les Is : ne comptez pas sur moi pour vous parler théorie et traitement du signal : mes résultats sont la plupart du temps basés sur des tests empiriques, et bien souvent, utiliser des techniques plus fines se sont avérées inutiles. En effet, si vous voulez du sample sur CPC, une seule règle : "allez-y comme des bourrins". Notre cher AY n'est pas très doué pour jouer des samples, d'une part à cause de ses 4 pauvres bits de volume, mais aussi du fait de leur courbe de volume logarithmique. Enfin, l'architecture de notre CPC fait qu'adresser l'AY est une opération particulièrement coûteuse en temps-machine...

Les samples
Commençons par le commencement : la terminologie. On parle de "sample" à tout bout de champ. "Sample" signifie "échantillon". Un échantillon est la partie atomique d'un signal échantillonné, soit dans notre contexte, une valeur. Je parlerai donc de "son" pour parler d'un signal (le contenu d'un fichier WAV par exemple), et de "sample" pour parler d'une valeur échantillonnée. Elle sera de 4, 8, 16, ou 24 bits selon la plateforme.

Dans un premier temps, il faut nourrir notre CPC avec de jolis sons. Dans 99% des cas, ils proviendront de sources externes, tels que des sons provenant d'un MODule, ou de WAVs de provenances diverses.

Notre PSG ne peut sortir que du 4 bits par canaux. Nos sons seront donc limités par ces 4 bits. Un avantage est que l'on pourra éventuellement les optimiser en taille : il est possible de coder deux samples en un octet. En pratique, ce ne sera utilisé que pour lire un sample brut. Si vous faites du Digitracking, où le temps machine devient critique, savoir quelle partie de l'octet doit être utilisé sera trop gourmand en temps machine. En revanche, il est toujours possible de stocker ces sons compressés, et de les décompresser une fois votre programme chargé (bien qu'utiliser un compresseur donnera probablement le même résultat !).

Quelle fréquence d'échantillonnage ? Là encore, le fruit de mes expériences montre qu'une fréquence d'échantillonnage entre 8 et 11Khz est largement suffisante sur CPC. Pour la petite histoire, j'avais testé des sons à 16Khz pendant la musique Digitrack de l'introduction d'Orion Prime : quelle déception lorsque je m'étais rendu compte que cela ne sonnait pas mieux qu'à 8Khz. De même le "meeeuh" échantillonné dans la CPC Meuuhting est joué à 44Khz, plus pour le challenge technique que pour la beauté du son : diviser sa fréquence par 4 n'aurait probablement pas changé grand chose. Il est donc parfaitement inutile d'utiliser une fréquence d'échantillonnage trop élevée. Pour vous donner un exemple : si vous souhaitez jouer un sample échantillonné à 20Khz, il faut jouer un sample 20000 fois par seconde. Mais admettons que votre code ne joue qu'à 10Khz : pour jouer le son dans la bonne fréquence, vous allez devoir lire un octet sur deux. Moralité : la moitié des octets encodés sont inutiles ! Idéalement, sur CPC, ayez une fréquence d'envoi de sample supérieure à la fréquence d'échantillonnage de vos sons, et vous aurez un résultat optimum.

Signé ou non signé ? La plupart des formats modernes utilisent des valeurs signées. Sur CPC, ce ne sera pas le cas. Ayant 4 bits à notre disposition, 8 sera "notre" 0, notre "milieu", avec bien sûr 0 pour valeur la plus faible, et 15 la plus forte.

Est-ce que les sons doivent être modifiés pour compenser la courbe logarithmique du AY ? J'avais longtemps expérimenté sur quelle courbe de conversion était la meilleure : Zik m'avait même pondu, grâce à un oscilloscope, une table précise convertissant une valeur 8 bits de volume linéaire vers la valeur 4 bits de volume logarithmique la plus proche. Mais il fallu se rendre compte de l'évidence : la qualité n'était pas améliorée. Moralité : inutile de perdre des cycles à passer par une table de conversion ! Balancez les octets au AY "comme un bourrin".

Ensuite, nous ne devons pas oublier notre cible : un pauvre speaker au pire, des enceintes bas de gamme au mieux (qui branche ses Cabasse sur son CPC ?). N'espérez pas y faire passer un large spectre de fréquences, surtout en 4 bits. Les fréquences basses nécessitent de la puissance ce que nous n'avons pas. Les fréquences aiguës peuvent être gardées, même boostées. Là encore, pas de miracle, seuls des tests vous dicterons quelles fréquences amplifier/atténuer, mais un simple filtre coupe-bas (à 100hz par exemple) pourra faire des miracles. Idéalement, effectuez ce travail sur chaque son séparément afin d'éliminer ce qui n'est pas utile, testez et écoutez les résultats isolément.

Dernière étape, et non des moindres. Voici LE secret pour obtenir de bons résultats : la compression. Bien que l'usage de la compression et la sur-compression soit un problème réel dans le monde de la musique de ce 21e siècle, c'est pourtant la solution à bien des problèmes de qualité sur CPC. En effet, tâchez de convertir un sample PC directement sur CPC : le résultat sera médiocre au mieux. Pourquoi ? Parce que l'AY n'a absolument aucune dynamique. 16 pauvres valeurs qui se battent en duel, contre 65536 au pire sur PC. Les octets du sample de base ne se rapprocheront que très rarement des limites (0 ou 65535), mais seront concentrés "vers le milieu". La conversion brute en 4 bits générera une masse d'octets allant de 6 à 9 au mieux. Pas assez pour reproduire quoi que ce soit. La solution est simple : prenez n'importe quel éditeur audio et augmentez le volume de 120, 150, voire 200%, idéalement après avoir normalisé le son (c'est-à-dire, l'avoir "étiré" sans provoquer de saturation). Ce traitement est également faisable en Basic, pour les puristes ! Testez ceci et regardez la différence. Quant à déterminer le niveau de saturation, c'est, comme bien souvent, au cas par cas.

Hand job
Et tout ça, à la main ? Si ça vous fait plaisir. Mais personnellement, je recherche l'efficacité. Il existe un outil cross-platform qui va vous faire gagner un temps monstrueux ce qui vous permettra de multiplier les essais et donc d'obtenir le meilleur résultat. Cet outil en ligne de commande s'appelle Sox. Je ne m'étendrai pas sur son utilisation, mais voici un exemple utilisé pour convertir une percussion d'Imperial Mahjong :

sox.exe samples/Bongo.wav generated/Bongo.wav bass -40 treble 26 gain 10 :
On créé un nouveau fichier son avec moins de basses et plus d'aigus (se referrer à la notice pour connaître les fréquences par défaut), et on amplifie le tout comme des bourrins.
sox.exe generated/Bongo.wav -b 8 -c 1 -r 8000 -e unsigned-integer -t raw generated/Bongo.raw :
Créé un fichier brut (sans header) en 8 bits, 8kHz, mono, non signé.


Seule la conversion de 8 bits vers 4 bits n'est pas prise en charge par Sox : vous pourrez la faire côté PC via un petit code python / C / Java ou tout ce que vous voulez (une simple division par 16 sera suffisante) ou même côté CPC, ce qui vous permettra d'avoir des sons 8 bits jusqu'au bout de la chaîne : si votre production souhaite utiliser les possibilités des cartes DigiBlaster et assimilés, tout sera prêt !

Samples ST
Une question de l'assemblée : comment l'Atari ST parvient à obtenir des sons 8 bits alors qu'il dispose du même processeur sonore ? Ils utilisent une astuce non faisable sur CPC. Le signal de sortie est la somme des volumes des trois canaux. Il "suffit" donc d'ajuster leur volume pour obtenir un son de précision supérieur. Par exemple, la différence entre les volumes 14 et 15 est très importante. Si on ajoute à ça une valeur de 1 sur un second canal, la somme sera donc légèrement plus forte que le volume 14. On a gagné en précision !

L'architecture du ST leur permet de modifier l'intégralité des registres du PSG en une instruction. Ils possèdent ainsi une table convertissant une valeur 8 bits en 3 valeurs 4 bits à envoyer aux trois canaux. En cumulant intelligemment les volumes, ils parviennent à obtenir une échelle pratiquement linéaire sur 8 bits.

Sur CPC, modifier le volume des trois canaux est escargotesque et ne donne aucun bon résultat (Crown a déjà expérimenté cette technique avec son ProTracker : passez en "mode 2" dans le logiciel et essayez de voir la différence !). Réfléchissons : admettons que le canal 1 soit la valeur principale, et le canal 2 soit censé ajouter de la précision avec une faible valeur. Il y aura un temps non négligeable entre l'envoi des deux valeurs : on imagine alors l'effet "escalier" se produisant alors qu'une seule valeur n'est attendue. L'instant suivant, le canal 1 reçoit un second sample : cependant, la valeur actuelle du second canal est toujours ajoutée à ce second sample ! Cela n'a aucun sens d'un point de vue sonore. Cette technique ne peut pas fonctionner sur CPC.

Autre avantage sur ST : étant mono, brancher une enceinte fonctionnera toujours, alors que sur CPC, le signal sera découpé en deux canaux. Au mieux, l'un sera bon, l'autre ne comportera pas le canal ajoutant de la précision. Moralité : ST wins.

Un peu de Z80 !
Il existe de nombreux articles indiquant comment envoyer un sample au PSG, je ne m'étendrai donc pas sur le sujet. Pour résumer, il suffit d'envoyer un volume sur un des canaux, et ce le plus rapidement possible. Idéalement, on ne sélectionnera le canal qu'une seule fois, au lancement de la routine, et il ne restera plus qu'à lui envoyer les valeurs.

; Sélection du canal (à ne faire qu'une seule fois).
ld bc,#f400 + canal
out (c),c
ld bc,#f6c0
out (c),c
ld bc,#f600
out (c),c

; Envoi de la valeur
ld bc,#f400 + valeur
out (c),c
ld bc,#f680
out (c),c
ld bc,#f600
out (c),c


Une optimisation toute simple consiste à utiliser un out (c),0 à la fin de chaque étape. Exemple avec la dernière étape :

ld bc,#f400 + valeur
out (c),c
ld bc,#f680
out (c),c
out (c),0


Comme Hicks l'a énoncé en disséquant Imperial Mahjong dans le précédent AW, une autre optimisation consiste à mettre le bit 7 à 1 dans toutes les valeurs des samples : le PSG n'en tient pas compte lors de l'envoi de la valeur sur le port #f4 (seuls les 5 premiers bits le sont), mais seuls les bits 6 et 7 sont pris en compte sur le port #f6 : on gagne un registre ! Je précise que cette astuce m'a été transmise par Grim lors de nos lointaines et mémorables conversations IRC vers 2005. Je suppose qu'il en est l'inventeur !

ld bc,#f400 + valeur
out (c),c
ld b,#f6
out (c),c
out (c),0


Attention lors du mix : additionner deux valeurs avec leur bit 7 à 1 va positionner la Carry, cela peut avoir des effets de bords sur votre code.

Enfin, lors de la sélection d'un registre, on peut éviter d'avoir à réserver un registre à #c0 : un out (c),b donne le même résultat ! On obtient dès lors :

ld bc,#f400 + canal
out (c),c
ld b,#f6
out (c),b
out (c),0


To mix or not to mix ?
Jouer un son est une chose, mais faire du Digitrack en est une autre. Doit-on jouer les samples sur les trois canaux, ou tout mixer en un ? La première option est possible, mais très couteuse en temps-machine : passer d'un canal à un autre coûte très cher et d'après mes tests, la perte de fréquence ne sera pas compensée par les 4 bits alors entièrement dédiés à chaque canal. La technique la plus efficace consiste donc à rester sur un canal (le second, afin d'être "au centre" de la stéréo) et de mixer deux ou trois sons. Et pourquoi pas 4 ? J'ai essayé : c'est possible mais ça commence a être sacrément dégoûtant. Le manque de registres nous oblige à jongler, la fréquence baisse, le résultat n'en vaut pas la peine.

Le mixage en lui-même est très simple : il suffit en théorie d'additionner les valeurs des samples et le tour est joué. En pratique, il faudra faire attention à éviter les débordements (j'ai une astuce pour ça mais je ne la dévoilerai pas ici : vous n'avez qu'à chercher par vous-même).

Jouer des notes
Jouer un son est bien gentil, mais comment faire varier la fréquence pour jouer un Do, Re, Mi, ou toute autre note ? Le principe est simple : au lieu de lire les samples avec un pas de 1, il faut les lire avec un pas de 1.1, 1.25, etc. en fonction de la note. Sachant que lire par pas de 2 jouera le son à l'octave supérieure, et lire un sample deux fois jouera le son à l'octave inférieure.

Deux questions : comment connaître ce "pas" ? Il peut être calculé, mais je ne vous le montrerai pas ici. Tout d'abord parce qu'il y a foule d'informations à ce sujet sur le net, et d'autre part parce que je ne l'ai jamais fait : je travaille uniquement à l'oreille, c'est bien plus rapide si vous êtes un peu musicien. Il ne s'agit que de trouver 11 valeurs après tout (car 12 notes par octave) ! Autre question, comment avancer de 1.2 ou 1.8 ? Les virgules, connaît pas en Z80 !

Virgule fixe à la rescousse
Il suffit d'utiliser ce que l'on appelle une "virgule fixe". Dans un registre 16 bits tel que HL, H sera la partie entière, et L la partie décimale. Pour avancer de 0,5, il suffit de lui additionner #0080 (#80 étant la moitié de 1, qui lui-même équivaut à #100) :

ld hl,#0000 ;HL est un offset sur le son à additionner.
            ;Ici, à 0, il indique qu'on n'avance pas.
ld de,#0080
add hl,de


Maintenant, HL est à #0080. H, la partie entière, indique qu'on est toujours sur le premier sample (car égal à 0). Avançons encore dans le sample:

add hl,de


HL est à #0100. H = 1, donc pointe maintenant sur le 2e sample ! C'est la victoire. Vous venez de lire un sample deux fois moins rapidement que l'original. Il suffit de faire varier DE pour reproduire toutes les notes des gammes.

Cette technique est simple mais donne de très bons résultats. Prodatron l'utilise dans son Digitracker, et il est assez facile de faire plus rapide. De plus, elle permet de gérer facilement les effets de Portamento en incrémentant / décrémentant DE.

Table de pas
Une autre technique que j'utilise dans Orion Prime permet un gros gain de temps machine, me permettant de monter à 18.3Khz (record !). Je me suis rendu compte par la suite que Crown (encore lui !) l'utilise également dans son Protracker, preuve qu'il avait vraiment très, très bien pensé son code a l'époque.

La technique consiste à précalculer une table de pas qui indique, pour chaque note, de combien d'octets avancer :

Par exemple, pour une note de base, elle aura cette tronche :
1, 1, 1, 1, 1, 1...
Pour une note de l'octave au-dessus :
2, 2, 2, 2, 2, 2...
Pour celle de l'octave en dessous :
1, 0, 1, 0, 1, 0


La technique de génération de cette table peut-être analogue à celle de la virgule fixe. Afin d'optimiser au maximum, chaque sous-table fera 256 octets : on peut ainsi utiliser le même pointeur d'incrément pour les trois canaux, sans se soucier du bouclage si l'incrément est 8 bits.

Deux inconvénients à cette technique : cette table de pas prend un peu de mémoire : en limitant les notes possibles à trois octaves (ce qui est suffisant pour la plupart des musiques Digitrack), et en réservant 256 octets pour chaque note, cela fait tout de même 9k, en plus des sons. Vous pouvez optimiser en ne stockant que les tables des notes utilisées dans votre musique. Autre inconvénient, le pas est bloqué à ce que vous lisez dans les tables. Donc, pas d'effet de pitch possible !

Enfin, j'ai récemment trouvé une technique encore plus rapide, sans limitation d'effet, que j'ai hâte d'utiliser dans ma prochaine production. Le but n'est pas de monter dans les hautes fréquences, mais plutôt d'utiliser le temps machine gagné pour faire d'autres effets PENDANT la lecture des samples.

Bouclage
Comment gérer le bouclage des sons ? Sur des machines modernes, on peut se permettre de tester, après avoir joué un sample, si on a atteint précisément la fin du son ou non. Or, le CPC ne dispose pas de la puissance processeur nécessaire pour tester la fin du son après chaque envoi PSG ! On se contente donc de tester la fin d'un son "quand on peut". Idéalement, essayez de faire cela à chaque VBL. Il vous faudra de toute façon lire les données de la musique à un moment ou un autre, c'est donc l'endroit idéal pour tester la fin des sons. Ce n'est pas extrêmement précis, mais suffisant dans la plupart des cas.

Sample-SID
Je reviens rapidement sur mes SID-samples utilisés dans Imperial Mahjong. Je vous dis depuis le début de l'article que jouer des samples sur 3 canaux n'est pas idéal du fait de la chute vertigineuse de la fréquence à laquelle les samples sont joués. Et pourtant, je le fais dans Imperial Mahjong ! Le cas des SID-samples est un peu différent car, malgré une fréquence plus faible, le résultat fonctionne malgré tout : l'onde générée par les samples module l'onde sonore générée par le PSG. Cette dernière n'étant pas limitée par notre code, la qualité du son produit reste correcte.

Une difficulté du SID-samples est l'utilisation de sons très courts devant boucler parfaitement. La technique du "je teste quand je peux", pourtant acceptable avec du Digitrack, n'est plus possible ici. Mais comment tester rapidement la fin des 3 sons des 3 canaux ? Faire une soustraction 16 bits, en plus de modifier l'état des registres, n'est pas envisageable. J'utilise donc une technique que j'aime beaucoup, utilisable dans bien des cas où vous devez parcourir une table qui boucle. Cette technique nécessite de la mémoire et l'utilisation de la pile. Admettons que je veuille jouer les samples suivants, en boucle : 0, 5, 10, 15

J'encode le tout de cette manière :

TableStart:
	dw 0
	dw $ + 2      ;Pointe sur les deux octets suivants.
	dw 5 * 256
	dw $ + 2
	dw 10 * 256
	dw $ + 2
	dw 15 * 256
	dw TableStart ;On boucle !


Il suffit d'encoder chaque valeur sur 16 bits, suivie d'une autre valeur 16 bits indiquant la position de la valeur suivante. Elle pourra se trouver par exemple deux octets plus loin (cas "normal"), ou au début de la table (bouclage). Il suffit alors de lire la table de la manière suivante :

TablePt: ld sp,TableStart
	 pop af   ;On récupère la valeur de notre sample dans A
	 pop hl   ;Nouvelle valeur pour le pointeur de la table.
	 ld sp,hl ;SP pointe sur notre nouvelle valeur.


Le bouclage est géré automatiquement ! Comme j'utilise des valeurs 8 bits, mes données sont multipliées par 256, sinon le POP AF placerait ma donnée dans le registre F du POP AF, ce qui ne m'avancerait pas à grand-chose. Un programmeur malin pourra placer autre chose que 0 pour F : on peut imaginer placer le bit 0 à 1 pour tester la carry, ou le bit 6 pour Z, à effectuer juste après le "ld sp,hl" ("pop hl"/"ld sp,hl" n'ayant pas modifié ces registres, c'est tout à fait "safe") !

L'inconvénient de cette technique est qu'elle prend beaucoup de mémoire. Mais dans le cas des Sample-SIDs, c'est absolument nécessaire pour gagner en temps machine. Pour information, les sons SIDs de l'introduction d'Imperial Mahjong prennent environ 60k ! Ils sont bien entendu générés avant de jouer la musique, à partir d'une onde de base.

Conclusion
Je crois avoir fait le tour de ce qu'il faut savoir pour jouer des samples de manière optimisée. N'oubliez pas qu'en son comme en toute chose, les idées saugrenues et les tests empiriques peuvent donner d'excellents résultats ! J'espère que cet article vous aura plu et appris quelque chose. A la prochaine !

Targhan/Arkos.