JESVS

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

El SID —el chip de sonido del Commodore 64, diseñado por Bob Yannes en 1981— es probablemente el chip de sonido más emulado de la historia. Y casi toda esa emulación vive sobre una pila GPL: libsidplayfp + reSID, el estándar de facto desde hace más de dos décadas. Esa pila es la que usa hoy HVSCDroid —el nombre en clave de la app acompañante que desarrollo en paralelo: un reproductor del HVSC (High Voltage SID Collection) para Android—. Mientras dependa de código GPL, la app no puede publicarse como código cerrado en Google Play. YANNES es mi respuesta: un emulador de SID 6581/8580 nuevo, escrito en C++20, fiel al sonido original, y con la intención de liberarlo bajo una licencia permisiva (MIT o Apache-2.0 — todavía no lo decido).

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

La regla que define el proyecto

YANNES existe bajo una sola regla: ninguna línea, tabla, constante o estructura puede venir de reSID, residfp, libsidplayfp o sidplay2. La historia de git debe ser 100 % libre de GPL. Las entradas permitidas son:

  • 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 no 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 es, honestamente, 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

Más de lo que esperaba tener a esta altura, aunque menos de lo que falta:

  • La librería (libyannes): C++20, sin dependencias externas, incluye el modelo del chip (envelope, waveforms, filtro, ruido, etapa de salida) y el entorno C64: un 6510 con opcodes ilegales, loader PSID/RSID, timers CIA, raster IRQ, y soporte multichip 2SID/3SID.
  • El harness: yannes_render (registros o tunes a WAV) y yannes_play (playback en vivo con muteo de voces en tiempo real — el modelo de la ruta de audio final).
  • El banco de regresión: 66 casos de test (tonos puros, barridos de filtro, familias de envelope, ruido, silencio, el “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. El banco es el driver del desarrollo: nada se da por bueno sin pasar por ahí.
  • 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.
  • Integración: ya 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

Esta es la parte divertida. El SID está documentado de forma notoriamente incompleta, así que gran parte de lo sabemos llegó por medición:

  • 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 sabes.
  • 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, 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 ( 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.
  • 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 — las hipótesis que pareaban todos los números y aun así estaban equivocadas:

  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. 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.

Y 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).

Qué falta

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

  • 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