Présentation

Le Ultra Low Complexity Codec (ULC) est un codec audio transformé, créé en 2019 par Aikku93. Il utilise une MDCT pleine bande et un emballage de coefficients sur 4 bits (nibble). Le décodage ne repose sur aucun codage d’entropie Huffman, ce qui réduit la complexité logicielle. Sur un Nintendo 64, il délivre du son 44,1 kHz stéréo à des débits comparables à Opus tout en n’occupant qu’environ 4 % du temps CPU du processeur principal de 93 MHz.

Contexte technique

Les cartouches N64 offrent entre 8 Mo et 64 Mo d’espace, alors que le stockage d’un morceau stéréo 44,1 kHz non compressé dépasse 12 Mo pour cinq minutes. Les solutions historiques – ADPCM via Libultra ou Opus accéléré par le RSP – réduisent l’encombrement mais restent trop gourmandes en cycles CPU pour un usage continu pendant le jeu. Les décodeurs MP3 ou MP2 nécessitent des fréquences de processeur supérieures à 80 MHz, hors de portée du N64.

Architecture du codec ULC

ULC segmente le flux PCM en tranches, applique une MDCT pleine bande, puis stocke chaque coefficient sous forme de nibble signé. Les valeurs -7 à -2 et 2 à 7 représentent les coefficients réels ; 0 indique un run de zéros (1‑16) suivi d’un nibble de longueur, 1 indique un run long (17‑288) suivi de deux nibbles, -1 signale un changement de quantificateur, et -8 encode un remplissage de bruit avec trois nibbles supplémentaires. Cette représentation élimine tout accès bit‑à‑bit et toute table Huffman, ce qui permet de décoder un bloc en moins de 150 lignes de C. Le format supporte même des débits de 16 kbps, bien que la qualité se dégrade.

// Exemple d’interprétation d’un nibble
switch(nibble){
  case 0: runZero = nextNibble()+1; break;
  case 1: runZero = ((nextNibble()<<4)|nextNibble())+17; break;
  case -1: quant = nextNibble(); break;
  case -8: noiseLen = nextNibble(); noiseAmp = nextNibble(); break;
  default: coeff = nibble; // -7..-2, 2..7
}

Implémentation sur le N64 et limites

Le premier portage a compilé le décodeur de référence sur le CPU principal, confirmant que la charge reste autour de 4 % pour du 44,1 kHz stéréo. Cependant, aucune partie du traitement n’est déléguée au RSP, le coprocesseur vectoriel de 93 MHz. Le RSP possède 4 KiB de mémoire de données (DMEM) et 4 KiB d’instructions (IMEM) et ne supporte que des opérations en point fixe 16 bits ou 32 bits. Pour exploiter le RSP, l’IMDCT doit être exécutée entièrement dans le DMEM, ce qui impose que le bloc de coefficients (typique 1024 échantillons) tienne dans 4096 octets après conversion en point fixe. Le transfert se fait par DMA depuis la RAM vers le DMEM, exécution du vecteur, puis retour en DMA vers le tampon audio. Cette approche réduit davantage la charge CPU, mais elle nécessite une implémentation point‑fixe soigneusement optimisée et une gestion stricte du débit de données afin d’éviter les débordements du DMEM.

En l’état, le codec ULC offre une solution viable pour la musique d’ambiance ou les dialogues en jeu, à condition que le développeur accepte la contrainte de coder l’IMDCT en point fixe et de gérer les limites de mémoire du RSP. Le code source est sous licence permissive, ce qui facilite son intégration dans des projets homebrew basés sur Libdragon.