Memory Full

Sampling et digitracking

Written by Targhan in April 2017

English version here

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.



Hi all! Welcome to Demoni... Oh, it seems there's been a change. This is a first! I am writing an article for Memory Full...

Hicks asked me to talk about samples. Let's have a broader view and also include Digitracking, that is, the art of playing samples on several channels.

Let's start by dotting our i's: don't count on me to talk about theory and signal processing: these are rather unknown to me, and my results come most of the time of empirical experiments. Very often, subtlety is useless on CPC. One rule would be “brute force!”. Playing samples is not the thing our dear AY does best, first because of its poor 4 volume bits, second because of their logarithmic curve. Third, addressing the AY is a slow operation, due to the CPC architecture...

Samples
Let's start at the beginning: terminology. What is a “Sample”? A “sample” is the atomic part of a sampled sound, that is to say in our context, a value. I will thus use the word “sound” to speak about the signal (the content of a WAV file for example), and “sample”, the sampled value. It can be 4, 8, 16, 24 or 32 bits depending on the hardware.

First, we have to feed the CPC with nice sounds. In most cases, they will come from external sources, such sounds from a MODule or WAVs found on the internet.

Our PSG can only output 4 bits per channel, we are thus limited by these 4 tiny bits. One advantage is that we could optimize the sound in memory: it is possible to encode two samples into one byte. Practically speaking, this is only useful for playing a raw sound. If you are digitracking, speed is critical, and knowing what nibble of the byte must be selected and played is too slow. However, nothing prevents you from storing the samples in this “compressed” way, and unpack them when your code is loaded (though I wouldn't both: a good compressor will probably give you the same result!).

What sampling frequency? There again, my experience shows that a frequency between 8 and 12khz is enough on CPC. For the record (pun intended), I had tested 16khz sounds for the introduction music of Orion Prime: how disappointed was I when I figured it didn't sound better than at 8khz! The same for the “moooo” from the CPC Meuuhting, played at 44khz, more for the technical challenge than for the sound quality: dividing its frequency by 4 wouldn't have changed anything. To sum up, it is useless to have a high sampling frequency. To give you an example, if you want to play a sound at 20khz, you must play 20000 samples per second. But if your code only replays at 10khz, to play the sound in its original frequency, you will have to read one sample out of two. Morality: half of the samples are useless! Ideally, on CPC, have a replay frequency higher than the sampling frequency of the sounds, and all will be fine.

Signed or unsigned? Most modern formats use signed values. On CPC, this won't be the case. With 4 bits at our disposal, 8 will be our “0”, our “middle”, with of course 0 being the lowest value, 15 the highest.

Should the sounds be processed to compensate the AT logarithmic curve? I have much experimented on which conversion curve was the best: Zik had even created, thanks to an oscilloscope, an accurate look-up table to convert an 8-bit linear curve to a 4-bit logarithmic curve. Strikingly, the conclusion to this was that the quality didn't improve. Don't spend cycles using a conversion table: directly send the samples to the AY (“no subtlety!”)!

Then, we must not forget our target: a small speaker, or low-end speakers at best. Don't expect outputting a large spectrum of frequencies, especially in 4 bits. Low frequencies require power, which we don't have. High frequencies may be kept, even boosted. There, no magical formula, only your tests will tell what frequencies should be amplified/reduced, but a simple low-cut (at 100hz for example) may be very effective. Ideally, do this on every sound separately to hear how it sounds afterwards. More on this later.

Last but not least. Here is THE secret to good-sounding samples on CPC: compression. Even though compression and over-compression is a real problem in nowadays music, it is a simple and effective solution to many sound problems on CPC. Indeed, try to convert a PC sound directly on CPC: the result will be mediocre at best. Why? Because the AY has no dynamic at all. 16 poor values against at least 65536 on PC. The original samples will almost never reach the limits (0 or 65535) but will concentrate in “the middle”. Raw conversion to 4 bits will generate bytes going from 6 to 9 at best. Not enough to sound like anything relevant. The solution is simple: take any audio editor, normalize the sound, then increase the volume of 120, 150, even 200%! Yes, it will saturate, but don't worry. Make some tests to determine what is the best level for this particular sound. Note that this process can be done in Basic! Purists will be happy.

Hand job
Must all this processing be done by hand, slowly and painfully? If you want to. But you know I'm an efficient person. A cross-platform tool will help you doing all this, at the speed of light. As you save time, you can multiply the tests and find the best result. The software is a command-line tool called Sox. I won't delve on how to use it, but here is an example I used to convert a percussion from Imperial Mahjong.

sox.exe samples/Bongo.wav generated/Bongo.wav bass -40 treble 26 gain 10
This creates a new WAV file with less basses and more trebles (please refer to the software manual to know the default frequencies), then amplifies the whole like crazy.
sox.exe generated/Bongo.wav -b 8 -c 1 -r 8000 -e unsigned-integer -t raw generated/Bongo.raw
This creates a raw sound file (without header), in 8 bits, 8khz, mono, unsigned.


Only the 8-bit to 4-bit conversion is not done by Sox: you will have to do it by yourself, for example via a C/Java/Python/whatever script. A simple integer division by 16 will be enough. You can also do it on CPC, which will allow you to have 8-bit sounds till the end: this would make possible the output to Digiblaster extension if you want!

ST samples
One question from a fellow attendee: how an Atari ST manages to play 8-bit sounds, whereas it has (almost) the same sound general as us? They use a trick that can not be properly done on CPC. The output signal is the sum of the volumes of the three channels. Thus, by cleverly adjusting their volume, they can reach a higher accuracy and get rid of the logarithmic curve. For example, the difference between the 14 and 15 volume is large. But if you set a volume of 1 on the second channel, with 14 on the first, you get a little more than 14, yet much less than 15. Better accuracy!

The ST architecture also allows to send all the registers to the PSG at once. Some guys have generated a look-up table converting an 8-bit value to 3 4-bit values to send to each channels. This table is built to get a near-linear 8-bit output.

On CPC, modifying the volume of the three channels is slow as hell and does not give any good result (Crown has already experimented this (with two channels) in his ProTracker: use the “mode 2” in the software, and try to hear the difference...!). Would it work on a CPC? Let's say the channel 1 is the main volume, and channel 2 is supposed to add some accuracy by setting a small volume. A non-negligible lag between the sending of the two volumes will happen: there will be a “stair” effect whereas only one “real” value was supposed to be heard. Some microseconds later, the channel 1 receives a new value. However, the channel 2 will still be added to the output! This makes no sense, sound-wise. This technique can not work properly on CPC.

One other advantage on ST: being mono, plugging a speaker will still work, whereas on a CPC, the signal will be split into two outputs. At best, one will sound right, the other will not have the accuracy bonus. ST wins.

A bit of Z80!
A lot of articles show how to send a sample to the PSG, so I won't explain this in detail. To sum up, you just have to send a volume to one channel, as fast as possible. Ideally, the channel is selected once, at the beginning of the code, then won't be modified anymore.

;Selecting the channel (done only once):
ld bc,#f400 + channel
out (c),c
ld bc,#f6c0
out (c),c
ld bc,#f600
out (c),c

; Sending a value
ld bc,#f400 + value
out (c),c
ld bc,#f680
out (c),c
ld bc,#f600
out (c),c


One simple optimization consists in using the “out (c),0” instruction at the end of last step. Example:

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


Like Hicks discovered by dissecting Imperial Mahjong in the latest Another World, an optimization consists in setting the bit 7 in every sample: the PSG ignores it when sending the value to the #f4 port (only the first 5 bits are used), but only the bits 6 and 7 are read by the port #f6: a register is saved! This trick was given to me by Grim during an old yet memorable IRC chat in 2005. I guess he invented it.

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


Watch out during the mix: adding two values with the bit 7 set will set the Carry. This may have some side-effect on your code (or not).

Finally, when selecting a register, you can avoid having to store the #c0 value: a “out (c),b” gives the same result! Thus:

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


To mix or not to mix?
Playing a sound is one thing, digitracking is another. Must we play the samples on the three channels, or mix them into one? The first option is technically possible, but very expensive in term of CPU: switching from a channel to another one is slow and from my tests, this loss of speed provokes a loss in replay frequency that the use of the full dynamic of the volume (4 bits) per channel does not repay. By far, the most efficient technique consists in using one channel (the second, to be in “the center” of the stereo) and to mix 2 or 3 channels. What about 4? I tried. It is possible, but it gets really ugly (Antoine also did: it was even uglier). The lack of registers forces us to juggle with registers a lot, the replay frequency lowers, the result sounds like crap.

The mixing itself is very simple: in theory, simply add the values of the 2 or 3 samples and everything is fine. In practice, watch out for overflow! Not as simple as it seems (I have a patented trick for this, but won't tell about it here: search for yourself!).

Playing notes
Playing a sound is nice, but how to vary the frequency to play a C, D, E or any other note? The technique is simple: instead of reading each sample with a step of 1, it must be done with a step of 1.1, 1.25, etc. according to the note. Know that playing the samples with a step of 2 will play the sound at the next octave, playing a sample twice, the lower octave.

Two questions: how to know this “step”? It can be calculated, but I won't show how here. First because there are many information about it on the net, second because I have never done it: I always work by ear, which is faster if you are a bit of a musician. Only 11 steps have to be found after all (because 12 notes per octave)! As for the second question: how to move of 1.2 or 1.8 bytes? How to do that in Z80 assembler?

The arrival of the fixed point number
We have to use what is called “fixed point arithmetic”. In a 16-bit register such as HL, H will be the integer part, L the decimal part. To advance of 0.5, add #0080 (#80 being half of #0100):

ld hl,#0000	;HL is an offset to the sound. 0 indicates we are at its beginning.
ld de,#0080
add hl,de


Now, HL is #0080. H, the integer part, indicates we are still on the first sample (because equals to 0). Let's make another iteration:

add hl,de


HL is now #0100. H = 1, thus points on the second sample! Victory is ours. You just read the sound twice slower than the original. We just have to vary DE to change to another note.

This technique is simple and gives good results. Prodatron uses it in his Digitracker, but it can be optimized even further. It also allows portamento effects by increasing/decreasing DE.

Step table
Another technique, which I use in Orion Prime, allows a significant gain of speed, allowing me to replay at 18.3khz (world record!). Later on, I realized Crown (him again!) used it too. He really had thought well about his Protracker when he did it.

This technique consists in precalculating a step table indicating, for each note, how fast to move within the sound.

For example, for the base note, it will look like this:
1, 1, 1, 1, 1, 1...
For the higher octave:
2, 2, 2, 2, 2, 2...
For the lower octave:
1, 0, 1, 0, 1, 0


Generating the table is analogue to the fixed-point technique. To optimize further, each subtable will be 256 bytes: the same pointer can be used for the three channels, without even having to manage the looping if the increment is done on 8 bits.

Two inconveniences about this technique: this step table takes a bit of memory: by limiting it to three octaves only (which is most of times enough for Digitrack music), and by allocating 256 bytes per note, it weighs 9kb. You can optimize it by storing only the subtables of the notes used by the song. Second inconvenience: the step is locked to what is read in the tables. No portamento effect is possible!

Finally, I have recently found an even faster technique, without effect limitation, which I hope to use in a production one day. The aim is not to play higher frequencies, but rather use the saved cycles to do effects DURING the replay.

Looping
How to manage the looping of sounds? On modern architecture, we have enough power to test, after each sample is played, whether the end of the sound is reached. On CPC, we certainly can not! As a fall-back, we check this “when we can”. Ideally, try to do that at the end of each frame. You will need to spend some time to read the data of the music at one time or another, so this will also be the moment to check the looping. This is not extremely accurate, but it is enough most of the cases.

SID sample
Let's quickly talk about the SID sample used in Imperial Mahjong. I told you at the beginning of this article that playing samples on 3 channels is too cumbersome, due to the cycle amount it requires on CPC. Yet, that's exactly what I do in Imperial Mahjong! It works because playing SID sample is a bit different. The wave generated by the PSG is accurate, only the one the SID sample generate is not. The overall quality is still acceptable even if the SID sample has a low replay frequency.

A difficulty about SID sample concerns the very small samples, that have to loop perfectly. The “checking whenever I can” technique explained above does not work anymore. So how to test the end of 3 sounds on 3 channels? Doing a 16-bit subtractions, besides modifying our registers, is too slow. I thus used a trick I love, and which you can use every time you have a looping table. It will however cost some memory, and requires the stack to be diverted. Let's pretend I want to play the following samples, in a loop: 0, 5, 10, 15.

I encode the whole like this:

TableStart:
        dw 0
        dw $ + 2      ;Points on the two next bytes.
        dw 5 * 256
        dw $ + 2
        dw 10 * 256
        dw $ + 2
        dw 15 * 256
        dw TableStart ;Let's loop!


You have to encode each value as 16 bits, followed by another 16 bits value pointing where is the next value. It can be two bytes further (“normal” case), or to the looping point. All we need to do is read the table this way:

TablePt: ld sp,TableStart
         pop af   ;Gets the sample in A
         pop hl   ;New value where to point to.
         ld sp,hl ;SP points to our new value.


The looping is automatically managed! As I use 8-bit values, my data is multiplied by 256, else POP AF would put it in F, which wouldn't be useful. However, a clever coder could use F! We can imagine setting the bit 0 (carry), or bit 6 (Z). Thus, just after the “ld sp,hl”, a JR C/Z could be done (“pop hl” and “ld sp,hl” do not modify F, so this is safe).

The main inconvenience of this technique is that it is memory-consuming. However, it was a life-saver in the SID sample context! For the anecdote, the SID sounds of the Imperial Mahjong introduction weigh 60k! They are generated just before the music starts, from a base wave sound.

Wrapping up
I think I told you all you needed to play beautiful samples, in an optimized way, on CPC. Don't forget that, in the audio domain just like in any other domain, strange ideas and empiric tests give excellent results! I hope this article was an interesting read. See you soon!

Targhan/Arkos.