JESVS

YANNES: escribir un emulador de SID 6581 desde cero, sin mirar el código GPL

SIDC64EmulaciónAudioClean-RoomRetro

El SID, el chip de sonido del Commodore 64 que Bob Yannes diseñó en 1981, es probablemente el chip de sonido más emulado que ha existido. Y lo curioso es que casi toda esa emulación se apoya sobre una pila GPL: libsidplayfp + reSID, que lleva siendo el estándar de facto más de dos décadas. Es justo esa pila la que usa hoy HVSCDroid, el nombre en clave de la app que estoy desarrollando en paralelo, un reproductor del HVSC (High Voltage SID Collection) para Android. El problema es obvio: mientras dependa de código GPL, no puedo publicar la app como software cerrado en Google Play. YANNES es mi respuesta a eso. Es un emulador de SID 6581/8580 escrito desde cero en C++20, fiel al sonido original, que pienso liberar bajo una licencia permisiva, MIT o Apache-2.0, todavía no me he decidido.

El nombre, claro, es un homenaje. Bob Yannes diseñó el SID en cuatro meses y siempre dijo que, en el fondo, era un sintetizador al que le habían puesto un ordenador encima.

La regla que define el proyecto

YANNES existe bajo una única regla: ninguna línea, tabla, constante o estructura puede venir de reSID, residfp, libsidplayfp o sidplay2. El historial de git tiene que ser 100 % libre de GPL. Las entradas permitidas son estas:

  • El datasheet del MOS 6581/8580 y sus application notes
  • Las entrevistas públicas de Bob Yannes
  • Documentación en prosa, artículos que describen el comportamiento con palabras
  • Observación en runtime: streams de registros y salidas doradas capturadas de HVSCDroid en ejecución, que tiene un tap de registros

Ese último punto es la pieza clave del método. La app, el lado “sucio”, HVSCDroid con su pila GPL, genera fixtures, solo datos, que fluyen en una sola dirección hacia YANNES, el lado “limpio”. Observar el comportamiento de un programa en ejecución no es copiar código. Cada decisión de comportamiento que no resulta obvia queda documentada en PROVENANCE.md: de dónde salió el número, cómo se derivó, qué se midió. Cuando el hardware implica una tabla, la tabla se deriva por fórmula y la derivación se registra. El registro ya tiene 56 entradas y, siendo honesto, es la parte del proyecto que más me ha enseñado.

Hay una advertencia de pie, documentada y asumida: el implementador, yo, con asistencia de IA, trabaja con disciplina de clean-room, pero la disciplina no es un escudo legal. Antes de cualquier release cerrado hace falta una revisión de IP humana. Por eso el plan de licencia sigue abierto.

Qué hay construido

La verdad es que llevo más adelante de lo que esperaba a esta altura, aunque todavía falta bastante.

La librería (libyannes) está escrita en C++20 sin ninguna dependencia externa. Incluye el modelo completo del chip —envelope, waveforms, filtro, ruido, etapa de salida— y también el entorno C64: un 6510 con sus opcodes ilegales, el loader PSID/RSID, los timers CIA, el raster IRQ y soporte para multichip 2SID/3SID.

El harness tiene dos piezas: yannes_render, que toma registros o tunes y los convierte a WAV, y yannes_play, que hace playback en vivo con muteo de voces en tiempo real. Este segundo es el modelo de la ruta de audio final.

Luego está el banco de regresión, que es donde de verdad se mide el trabajo. Son 66 casos de test —tonos puros, barridos de filtro, familias de envelope, ruido, silencio, el famoso “ADSR bug”— que se renderizan por ambos motores, YANNES y el oráculo de referencia, y se puntúan con métricas de correlación de envelope, ganancia por banda y forma espectral. Nada se da por bueno sin pasar por ahí. Es el driver del desarrollo, literalmente.

En cuanto a rendimiento, tras una pasada de optimización, renderizar a 48 kHz cuesta ~3,5 % de un núcleo de escritorio, resampler incluido, con salida byte-idéntica. Seis veces más rápido que la primera versión.

Y ya hay integración: está vendeado dentro de HVSCDroid detrás de un flag, y un build de debug renderizó un tune real del HVSC del propio teléfono con paridad bit-exacta contra el host (439 904 muestras, maxdiff 0).

Lo que el 6581 nos ha enseñado

Aquí viene la parte entretenida. El SID está documentado de forma notoriamente incompleta, así que gran parte de lo que sabemos llegó por medición directa.

El reloj de referencia corre desviado. Midiendo contra el oráculo, el “1 MHz” del C64 PAL son 985 033,6 Hz reales en nuestra referencia, unas 217 ppm por debajo del nominal. Suficiente para desafinar un A/B si no lo tienes en cuenta.

El ruido no avanza cada ciclo. El LFSR del generador de ruido avanza un paso por flanco de subida del bit 19 del acumulador del oscilador, no en cada reloj como sugiere una lectura ingenua del datasheet. Con ese cambio, la ganancia del caso de ruido pasó de 0,26 a 0,95–0,99. Y el truco de Hubbard de usar la voz de ruido como percusión afinada tiene sentido: la “voz de ruido” es en la práctica un carrier triangular a la frecuencia del oscilador.

El envelope es un divisor exponencial por tramos. La velocidad de decay no es constante por nivel: la curva t(env) medida tiene un quiebre, un divisor exponencial por piezas, y el ataque es de frente cargado, rápido al principio y no lineal. Instrumentamos la lectura de ENV3 desde el propio driver para medir la curva del divisor exacta.

El “flicker” del ADSR tiene una matemática propia. El famoso bug del envelope en el 6581, el toggling del gate rápido, produce re-ataques en un ciclo límite de ~3,1–3,3 s que crece conforme el piso del envelope baja, y los dropouts se sueltan al ritmo del decay, no del release. Sin este comportamiento, los drones de Lightforce de Rob Hubbard pierden el brillo.

El filtro 6581 es raro y medible. El FC bajo tiene una meseta: por debajo de cierto punto la frecuencia de corte no baja de ~1,1 kHz en la práctica (midamos RMS constante barriendo FC a un carrier de 481 Hz). Las superficies de respuesta LP/BP/HP por celda de (FC, resonancia) se ajustan como biquads empíricos, no hay forma analítica identificable, son curvas del silicio.

Y hay detalles que hacen el timbre: asimetría de DAC por voz (que explica los armónicos pares del triángulo), la forma débil de las formas de onda combinadas, una ruta de DC con dos constantes de tiempo acopladas, y el orden de bits del selector de waveform, donde el datasheet tenía razón y yo no.

Los tropiezos (que son lo más valioso)

Un proyecto así se mide por sus retracciones, por las hipótesis que encajaban todos los números y aun así estaban equivocadas. Hubo varias.

  1. “El filtro se auto-oscila” — no. Era un archivo de salida viejo que no se había sobrescrito. El oráculo mudo, leído en fresco, estaba en silencio. La lección: siempre verifica que lo que estás analizando es lo que acabas de generar.
  2. “El flicker es una carrera ataque-frame” — retractado por un error de unidades de 1000×: una constante estaba en segundos totales y la leí como por paso. Las hipótesis bonitas se comprueban aritméticamente antes de celebrarlas.
  3. El comparador de pulso “estrictamente menor” — mejoraba el score, pero un pulso y su complemento tienen espectros de magnitud idénticos: una inversión de polaridad pasa cualquier métrica de magnitud. Hace falta un test sensible a fase en el lazo antes de tocar el comparador.
  4. La higiene del scorer. Las métricas mismas mienten si no se cuidan: ventanas de ganancia mal recortadas y clicks no excluidos inflaban el puntaje. Reparametrizamos el scorer y el marcador bajó de 23 a 18 — y ese 18 honesto vale más que el 23 acomodado.

También hubo trampas del oráculo: el all-muted no es silencio (la ruta filtrada sigue viva), las voces no son lo que el número de control dice a primera vista, y el tune número que recibe INIT es 0-based, no 1-based. Un off-by-one clásico que nos costó una tarde entera.

Qué falta

El marcador honesto está en 20 de 66 casos, y cada uno de esos pases se lo ganó. Lo que sigue, en orden aproximado:

La cola de fidelidad. El exceso sub-100 Hz de Lightforce en la sección de drones/timbales, el exceso de eventos en decays rápidos, modelar el ciclo límite del ADSR bug (fenomenología ya medida, modelo pendiente), la forma espectral del piso de ruido, y los residuales de fase/comb del filtro.

El modelo 8580, bloqueado por ahora porque el oráculo misdetecta ciertos tunes. Hay un knob pendiente del lado sucio.

El oráculo de hardware. Acabo de conseguir un C64 breadbin real con 6581. Todo el rig está listo y testeado en emulación: PRGs de banco que instalan su propio raster IRQ, un puente por el userport con protocolo propio, ingesta de capturas al formato del banco. La máquina viene en camino. La primera campaña es validar la cadena y luego re-medir las superficies del filtro, el clocking del ruido y los crests de los timbales en silicio. Pasar de “coincide con reSID” a “coincide con el chip” es el paso cualitativo grande que queda.

El cutover. Swap de la ruta de audio en vivo de HVSCDroid al motor YANNES, soak largo en el dispositivo, y retirar la pila GPL del APK. Ningún APK con GPL ha shipeado, así que vamos compliant.

Decidir la licencia. MIT por simplicidad, o Apache-2.0 por la cláusula de patentes y las NOTICE files. No lo sé todavía; es una de las decisiones que acompañará la revisión de IP.

La meta de v1.0 está escrita y es verificable: T1 10/10 del banco, todo caso T2 en PASS o con residual documentado y medido; el audio en vivo del dispositivo corriendo YANNES; CPU igual o mejor que el motor de referencia; API congelada; revisión de IP completa. Hasta entonces, 0.x.

Por qué hacerlo así

Podría integrar cualquier emulador existente y terminar. Pero hay algo satisfactorio en el ejercicio: el SID es un chip con la personalidad de su diseñador, atajos de ingeniería de 1981 que se convirtieron en el instrumento de toda una generación de compositores de demo y game music. Redescubrir esos atajos por medición, con un registro de dónde salió cada número, es la clase de proyecto que quería hacer. Y si al final queda una librería de emulación de SID moderna, rápida, medible y con licencia permisiva que cualquiera pueda meter en su app cerrada, mejor.

El repo mantiene su registro abierto: PROVENANCE.md con cada derivación y retracción, docs/PLAN.md con los hitos. Cuando la licencia se decida y la revisión de IP pase, se publica. Mientras tanto: 20/66, subiendo.

← volver a posts
↑