[CROSSOVER]

LabVIEW e SID: sviluppo del chip audio nell’emulatore del C64

13 Jul 2025 - Visualizzazioni: 441 - Autore: Ale914

Ovviamente è successo.

Nel post precedente avevo scritto di voler provare a creare un emulatore di C64 scritto interamente in LabVIEW.

Avevo fatto una scaletta ragionata di quali parti implementare prima, in base alle loro dipendenze reciproche e alla complessità di sviluppo, e il SID (il chip audio) era all'ultimo posto - l'ultimo.

E invece è successo che l'ho implementato per primo.

Era in effetti impossibile resistere alla tentazione di fare suonare sul SID emulato la demo "A mind is Born".

Quindi, ignorando l'ormai obsoleta scaletta e la logica delle cose che avrebbe voluto che si procedesse in modo opposto, ecco come sono andate le cose.

FAST FORWARD

Se non ti interessa leggere l'intero post ma vuoi andare dritto al codice LabVIEW del SID, ecco il link al progetto su Github - C64 LabVIEW Emu

Il SID

Il SID è il chip del Commodore 64 che si occupa della generazione audio. Progettato da Bob Yannes nel 1981, il SID offre un suono sintetizzato distintivo e ancora oggi molto amato. Le caratteristiche principali sono:

  • - Tre voci indipendenti. Ogni canale supporta forme d’onda:
triangolare, dente di sega, impulso, e rumore
  • - ADSR Generatore di inviluppo (Attack, Decay, Sustain, Release)
  • - Filtro analogico multimodale Low-pass, High-pass, Band-pass, con risonanza regolabile
  • - Modulazione tra oscillatori: ring modulation e sync

Il SID è un chip interamente digitale sia nella generazione del suono che nell’interfacciamento con la CPU. Tutti i parametri audio sono controllati via registri I/O, e la sintesi si basa su logica digitale, senza componenti analogici interni, ad eccezione del filtro.

Di seguito il diagramma a blocchi del SID, fonte https://www.c64-wiki.com/wiki/SID

Diagramma a blocchi del SID

Il SID, da solo

La prima cosa da capire era come fare ad "alimentare" il SID con dei dati visto che il resto delle parti del C64, soprattutto la CPU e la Memoria, non le avevo ancora implementate. Per fortuna esistono altri emulatori molto validi in giro e ad esempio VICE permette di esportare in un file il dump dei registri del SID mentre sta suonando.


x64sc -soundrecorddev dump -soundrecord sid_dump.txt 
-autostart A_Mind_Is_Born.pr

Questo comando genera un file di testo con i valori istantanei dei registri del SID campionati a 48 KHz. Ogni istante è rappresentato in un formato come quello seguente:


FREQ: 0602 dc00 100b
PULSE: 0900 0002 0e33
CTRL: 41 11 60
ADSR: ffd0 ffe0 fff5
FILTER: 0007 RES: ff MODE/VOL: 1f
ADC: ff ff
OSC3: 00 ENV3: 00

A questo punto ho dovuto decimare il file, il mio target era avere un SID emulato che eseguisse con un clock di 1ms quindi le informazioni nel file erano 48 volte maggiori in quantità a quelle che mi servivano. Ho scritto un piccolo VI per ridurre la risoluzione del dump a 1KHz, anche per avere un file di dimensioni più facilmente gestibile, il file originale generato da VICE era infatti di circa 500 MiB.

Pre processing dei registri

Il passo successivo è stato il parsing delle informazioni di ogni singolo frame, quello che volevo ottenere erano delle informazioni pre processate per alimentare di dati il SID emulato, ad esempio convertire i registri FREQ in Hz oppure l'ADRS in tempi in secondi, questo per alleggerire il lavoro del SID durante la generazione audio. Anche se la struttura e le informazioni contenute nel dump sono abbastanza intuitive da capire, è servito studiare i registri del SID per renderle usabili dall'emulatore.

A questo scopo sono state molto utili le specifiche originali del SID 6581 che descrivono il significato e il formato delle informazioni contenute nei registri del SID, formato che è stato rispettato da VICE nella generazione del dump quindi il parsing dei valori è quasi 1:1, ovviamente con qualche eccezione.

Ad esempio, per convertire i registri FREQ in Hz va usata questa formula

    F(Hz) = FreqReg × (Clock / 2^24)

Dove clock è la frequenza a cui gira il SID, se nel formato PAL è 985248Hz e i 24 bit sono la dimensione del registro accumulatore di fase degli oscillatori interni al SID. Detta così sembra banale, ma arrivare a queste informazioni ha richiesto una disceta dose di archeologia tecnica.

Nessun problema per interpretare le informazioni di PULSE, CTRL e ADSR che sono descritti bene nelle specifiche del SID, l'ADSR addirittura con qualche esempio di uso musicale.

Interpretare FILTER invece è più complesso, sulla carta ogni valore del registro mappa una lookup table con le frequenze di taglio, ma il SID non era famoso per essere stabile e lineare per quanto riguarda il filtro. Quindi, nella pratica, ho dovuto usare una lookup table diversa da quella riportata nella specifica, derivata dal progetto reSID un emulatore del SID molto fedele e che è di fatto il riferimento. Chi ha sviluppato reSID ha caratterizzato il filtro di vari chip SID e calcolato una lookup table che descrive il comportamento "medio" del filtro dei SID ma che è comunque molto fedele nel suono che producea.

ADC e OSC li ho ignorati, essendo dei registri in "uscita" dal SID non mi interessavano per l'emulazione.

Il risultato finale del pre processing sono 4 array di cluster LabVIEW, 3 per le Voci e 1 per il Filtro. Ora manca solo fare suonare questi dati.

ADSR

La prima funzione che ho implementato è stata l'ADSR, in pratica è un modulatore di inviluppo che ha 4 fasi: attack, decay, sustain, release. Ogni fase ha le sue regole e il suo parametro dedicato per definirne il funzionamento, l'output dell'ADSR è un valore float da 0 a 1 che viene utilizzato per modulare (moltiplicando) il valore del relativo oscillatore. Il modo più naturale che mi è venuto in mente per implementare l'ADSR era tramite una piccola macchina a stati, l'ADSR infatti evolve di stato in stato allo scadere dei tempi dei suoi registri oppure quanto il GATE cambia valore.

Il VI ADSR è impostato come reentrant pre allocated dovendone avere 3 in parallelo in esecuzione, ognuno con i suoi dati.

E' stato abbastanza semplice da implementare, quello che volevo ottenere era però che fosse semplice e veloce, quindi ho eliminato a ogni iterazione di revisione del codice, tutte le complessità superflue.

Le specifiche del SID hanno aiutato a capire bene cosa facesse l'ADSR dato che sono corredate con un Annex che descrive in dettaglio il suo funzionamento, con tanto di grafici dell'inviluppo generato in diverse condizioni.

FILTRO

Il filtro del SID è magia nera, nessuno sa esattamente come funzioni e soprattutto non c'è un filtro uguale all'altro, ogni chip suona in modo specifico. Come detto prima, per approssimarlo meglio ho usato, invece della curva teorica, quella caratterizzata dal progetto reSID e questo ha aiutato molto a ottenere un suono "musicale".

Nello specifico ho usato la curva del SID 8051 che è quello che suona meglio "A Mind Is Born" la demo che ho usato come benchmark per creare l'emulatore.

Tecnicamente il filtro che ho usato nell'emulatore è un Chebyshev con 12dB/ottava di attenuazione, come nel SID vero, e il controllo parametrico della risonanza. Ho implementato anche tutte le funzioni accessorie del filtro del SID 8051 incluso il routing delle singole voci verso il filtro o direttamente verso l'output, la scelta del tipo fi filtraggio LP, HP, BP e le varie combinazioni di questi.

Devo dire che l'implementazione del filtro non mi soddisfa al 100% e sicuramente ci dedicherò altro lavoro, La cosa che mi piace meno è che ho dovuto implementare uno sorta smooting del cambio frequenza di cutoff per evitare che il filtro "innescasse" generando per qualche momento degli sgradevoli "fischi".

OSCILLATORE

Questo è il cuore del SID, la parte che genera i suoni. Ogni oscillatore SID supporta 4 waveform, selezionabili via registri. L'implementazione iniziale l'avevo fatta usando gli oscillatori NI presenti in LabVIEW. In pratica ogni oscillatore SID conteneva 4 oscillatori NI, uno per ogni voce, che generavano in parallelo. In cascata un case selezionava quale waveform portare verso l'output. L'implementazione funzionava al 90% ma i problemi, come al solito, sono nei dettagli.

Tre caratteristiche rendono il SID distintivo:

  • - Generare più waveform contemporaneamente da una solo oscillatore
  • - Ring modulation
  • - Hard Sync

Mi sono presto reso conto che usando gli oscillatori NI era impossibile implementare bene queste caratteristiche.

Inoltre l'uso della CPU con gli oscillatori LabVIEW era eccessivo, in pratica ne eseguovano 12 in parallelo, 4 x 3 voci.

OSCILLATORE, fatto in casa

Volevo creare un oscillatore che fosse più simile possibile a quello del SID reale con cui poter costruire un emulatore fedele. Dopo una ricerca di informazioni durata qualche giorno e che mi aveva creato più confusione che altro (c'è molta mitologia intorno a come funzioni esattamente un SID al suo interno) mi sono imbattuto in una intervista al progettista del SID chi meglio di lui per sapere come funziona un SID?

Leggendo parola per parola mi sono messo a implementare un nuovo oscillatore. La base dell'oscillatore del SID è un accumulatore di fase a 24 bit che incrementa continuamente di un valore dipendente dalla frequenza che deve generare. Da questi 24 bit si derivano tutte le waveform possibili, saw, triang, pulse e noise.

La forma saw è creata prendendo solo i 12 bit più significativi dell'accumulatore, la risoluzione del D/A del SID è infatti da 12 bit, quindi i 12 bit dell'accumulatore vengono inviati direttametne al D/A.

La forma triangolare è anche lei generata a partire dai 12 bit più significativi dell'accumulatore di fase, il MSB viene usato come "invertitore" e gli altri 11 come valore della waveform, quando MSB = 1 viene fatto una XOR tra gli 11 bit e 0xFFF, questo inverte il valore degli 11 bit cambiando di fatto la direzione alla rampa che inizia ad andare verso il basso dopo avere raggiunto il suo massimo (MSB = 1). Ne deriva una forma triangolare con la stessa frequenza della saw ma con ampiezza dimezzata, 11 bit di valore invece che 12. Basta fare uno shift di 1 bit per guadagnare ampiezza, al costo di perdere 1 bit di risoluzione.

La forma pulse viene creata sempre dai 12 bit più significativi dell'accumulatore di fase e da un comparatore i cui valori può andare da 0 a 4095. Quando i 12 bit valgono più del comparatore, tutti i 12 bit dell'out vengono messi a 1, altrimenti sono tutti forzati a 0. Questo genera una forma pulse a cui è possibile cambiare il duty cycle o pulse width come lo chiama il SID.

Discorso completamente diverso per il generatore di noise, in questo caso non viene più usato l'accumulatore di fase come sorgente di valori ma un LFSR a 23 bit con 4 TAP, dall'LFSR vengono poi estratti i 12 bit più significativi che rappresentano il valore della waveform. L'LFSR viene "cloccato" con un bit "centrale" dell'accumulatore di fase per aggiungere la componente della frequenza impostata sull'oscillatore al noise.

Ecco il codice del nuovo oscillatore, qui in alta risoluzione

LabVIEW SID oscillator

Dopo avere implementato il nuovo oscillatore sono tornato a provare a implementare le tre caratteristiche distintive dette prima. Combinare le waveform generate da un solo oscillatore viene sconsigliato dal progettista del SID perchè portebbe generare effetti incontrollabili, però molte demo sfruttano questa funzione per creare suoni "speciali" quindi dovevo almeno provarci.

Ovviamente essendo una pratica sconsigliata, il progettista non spiega nemmeno come è implementata nel SID quindi sono in un vicolo cieco. Da Claude e su vari forum leggo che la combinazione tra waveform è di fatto un AND bit a bit dei 12 bit finali di ogni waveform, provo a implementarlo ma, come tutte le cose semplici, non funziona e anzi produce suoni inascoltabili.

A questo punto torna in aiuto reSID, anche loro hanno dovuto affrontare il problema e questa è la conclusione a cui sono arrivati:

This behavior would be quite difficult to model exactly, since the SID in this case does not act as a digital state machine. Tests show that minor (1 bit)  differences can actually occur in the output from otherwise identical samples from OSC3 when waveforms are combined. To further complicate the situation the output changes slightly with time (more neighboring bits are successively set) when the 12-bit waveform registers are kept unchanged.

It is probably possible to come up with a valid model for the behavior, however this would be far too slow for practical use since it would have to be based on the mutual influence of individual bits.

Anche in questo caso hanno creato delle lookup table per 4 diverse configurazioni di combinazioni di waveform - le altre combinazioni possibili semplicemente producono degli zeri, nessun suono - ho importato le tabelle reSID e il risultato è stato quasi perfetto, molto simile a un vero SID.

Implementare Sync e Ring Modulation col nuovo oscillatore è stato invece semplice. Nel Sync quando il bit MSB di un oscillatore arriva a 1 la fase di un altro oscillatore "collegato", viene resettata. Nel Ring Modulation sempre quando MSB di un oscillatore va a 1 i valori in uscita da un altro oscillatore vengono invertiti di segno.

PLAY

A questo punto, per fare funzionare tutte le parti avevo bisogno di costruire un piccolo player che "pompasse" i dati dei registri dumpati dentro l'emulatore. Ho creato un VI con 3 loop:

  • - generatore audio, verso la scheda audio del PC
  • - emulatore SID: 3 ADSR, 3 Oscillatori e il filtro
  • - generatore di dati per il SID, èer "sparare" i dati dei registri verso il SID in brute force

E funziona, l'audio quasi magicamente viene generato dal player e la mitica "A Mind Is Born" finalmente suona.

Ho creato un video del player in azione.

Sotto una preview della GUI del player.

LabVIEW SID emulator