Tengo un DAC en el escritorio, una suscripción a TIDAL y la terminal abierta todo el día. Lo que no tenía era una forma de escuchar TIDAL a 24 bits desde ahí, con la seguridad de que lo que llega al DAC es exactamente lo que salió del archivo, sin que nada en el camino lo remuestree.
Así que decidí hacer uno. Se llama phonia, está escrito en Rust y lo empecé el 15 de septiembre. Al cierre de septiembre ya reproduce, busca, abre álbumes, artistas y mi biblioteca, todo en modo bit-perfect. Pero en este post no quiero hablar tanto del reproductor como de la forma en que lo hice: casi todo el código lo escribió una IA y yo me encargué de dirigir. Lo que hizo que eso funcionara no fue el modelo, sino el método.
Detalle técnico: phonia es un cliente no oficial de TIDAL, para uso personal, y necesita una suscripción propia. No tiene relación con TIDAL, no descarga música ni evade ningún DRM: pide el stream igual que cualquier reproductor y lo envía a la tarjeta de sonido.
Primero el plano, después el código
El primer día casi no hubo código. Hubo un plan: seis fases (prototipo en CLI; daemon y motor de reproducción; interfaz de terminal; calidad de audio; funciones tipo SONE; extras y empaquetado) y 45 issues abiertos en GitHub ese mismo 15 de septiembre, cada uno en su milestone. Desde las letras sincronizadas hasta el paquete para Fedora, todo quedó escrito antes de saber si la primera pieza iba a sonar.
Eso le dio a la IA algo que no tiene por sí misma: una dirección que no depende de lo que recuerde de la sesión anterior. Cada issue (o cada parte, si es grande) va en su propia rama NN-nombre, que sale de develop y nunca de otra rama abierta. No apilo PRs, porque una rama apilada no se puede fusionar por separado y, si se borra la base, queda huérfana. Voy una pieza pequeña a la vez.
Al cierre de septiembre, el repo lleva 109 commits y 47 pull requests fusionados.
Cómo se reparte el trabajo: Opus, Sonnet y yo
El reparto es sencillo. Cuando algo implica decisiones de diseño reales (un subsistema nuevo, un cambio de protocolo, algo de la interfaz con ventajas y desventajas), lo planea Opus, el modelo más capaz, que me entrega un plan corto con cada decisión abierta señalada. Yo decido y, una vez aprobado, un subagente con Sonnet, más rápido, lo implementa. Lo que ya está bien acotado pasa directo a implementación.
La regla que más me ha servido es esta: nada se commitea, se sube ni se abre como PR sin mi visto bueno en esa sesión. Aprobar una pieza no significa aprobar la siguiente.
Hay otro detalle que aprendí pronto. El asistente tiene su propia memoria entre sesiones, pero vive en mi máquina; si cambio de computadora o de modelo, esa memoria se queda atrás. Por eso la fuente de verdad está en el repo: un ROADMAP.md con lo que está hecho y lo que falta, un docs/DECISIONS.md con el porqué de cada decisión (fechado, y sin editar las anteriores para ocultar que algo cambió) y una sección "Working process" en el README que explica cómo se trabaja. Cualquiera, persona o modelo, debería poder retomar el proyecto a mitad de una fase sólo con eso y los issues.
Detalle técnico: los commits y PRs no llevan el trailer
Co-Authored-Byde la IA; es una instrucción mía, documentada en el README. No es para ocultar nada (este post existe precisamente para contarlo), sino para que el historial refleje qué cambió y no quién lo tecleó.
Las decisiones que me quedé yo
La IA escribe rápido, pero hay preguntas que no le corresponde responder. Estas las tomé yo.
PKCE o nada. TIDAL tiene un login "de dispositivo" muy cómodo, pero con él nunca se obtiene el permiso de HiRes, aunque la cuenta lo tenga. Sin ese permiso no hay 24 bits, y sin 24 bits el proyecto no tiene sentido. Por eso el login es PKCE, aunque sea más laborioso.
Bit-perfect de verdad. En modo exclusivo, phonia toma el DAC y le entrega las muestras sin modificarlas. Pero no se lo quita a nadie a la fuerza: lo solicita por D-Bus con la misma convención que ya usan PipeWire y PulseAudio para pasarse las tarjetas, y lo libera al pausar o cuando otro programa lo pide. Para todo lo demás (bocinas de la laptop, audífonos Bluetooth) está el modo compartido, que pasa por PipeWire, puede remuestrear y lo indica: la interfaz nunca lo presenta como bit-perfect.
Daemon y cliente. Quien reproduce, guarda la sesión de TIDAL y consulta el catálogo es un daemon, phoniad. La TUI es sólo un cliente: depende únicamente del crate phonia-ipc, así que se compila y se prueba sin ALSA, sin D-Bus y sin red. Incluso la búsqueda pasa por el daemon, para que la sesión tenga un solo dueño.
Detalle técnico: el protocolo es JSON delimitado por saltos de línea sobre un socket Unix, versionado
major.minor(va en la 1.6), así quesocat - UNIX-CONNECT:$XDG_RUNTIME_DIR/phonia/phoniad.sockya funciona como cliente. El gapless (una pista pegada a la otra, sin silencio entre ellas) se verificó bit por bit contra un álbum real de TIDAL, grabando la salida con un loopback de ALSA (snd-aloop) en lugar de confiar en el oído.
Con esa base ya hay algo que se puede usar:
Lo que no salió tan bien
No todo fue sencillo, y vale la pena contarlo porque ahí se nota qué hace bien la IA y qué no.
El README todavía dice "phase 0". El título se quedó así desde la primera semana, cuando phonia era un prototipo de una sola pasada. Todo lo de abajo ya se actualizó, pero el encabezado no. Es el tipo de cosa que ni la IA ni yo revisamos porque dábamos por hecho qué decía.
Una librería con un error tipográfico. Uso tidlers como cliente de TIDAL, y en los favoritos apareció un parámetro mal escrito, ofset, que rompe la paginación después de la primera página. La solución fue dejar de usar sus llamadas de búsqueda y biblioteca y consultar la API directamente por HTTP, con la misma sesión autenticada. Además, el daemon traduce las respuestas a tipos propios, así que si tidlers cambia, los clientes no se ven afectados.
Un bug que apareció por otro lado. Al construir la biblioteca (#98), una prueba descubrió algo que ya estaba roto desde antes: al mover el cursor dentro de un álbum o artista ya abierto, la pantalla no se redibujaba, porque la comparación que decide si algo cambió no tomaba en cuenta ese cursor. Se corrigió de una vez para la búsqueda y la biblioteca.
CI y formato, tarde. El 17 de septiembre abrí #50 porque el código nunca había pasado por rustfmt: cargo fmt --check mostraba 39 bloques de diferencias. No se resolvió sino hasta el 26, junto con la CI. Fue más de una semana de PRs con ruido de formato que se pudo evitar desde el primer día.
El idioma, a medias. Empecé en español y a los dos días decidí que el proyecto sería en inglés (#47). El código y la documentación se tradujeron, pero los primeros 45 issues y los primeros commits se quedaron en español. El historial es bilingüe y así se va a quedar.
Los GIFs de este post. Le pedí a la IA unos guiones para grabarlos y los escribió con mucha seguridad, pero con supuestos: apuntaban a binarios de depuración que no existían, arrancaban el daemon con un comando mal copiado y daban por hecho que Enter ponía a sonar un álbum. La primera tanda salió con la cola vacía y el mensaje "Trying again…". Un guion que la IA escribe y nadie ejecuta es sólo una hipótesis, así que tuve que revisar los cuadros uno por uno, igual que con el código.
Detalle técnico:
cargo test --workspacetiene que poder correr en cualquier lado sin tocar hardware ni cuentas reales. Lo que necesita el DAC, mi login de TIDAL o un servidor de sonido se marca con#[ignore]y se corre a mano. Y hay una regla escrita en mayúsculas para la IA: nunca usar--include-ignored, porque abre la tarjeta de sonido real. Para probar audio se usa un archivo mudo, un sink nulo de PipeWire osnd-aloop, nunca las bocinas.
En qué punto está (y qué falta)
Al cierre de septiembre, las fases 0 y 1 están completas. La fase 2, la interfaz, está terminada salvo las portadas en la terminal (#24), que dejé para el final a propósito porque traen la dependencia más pesada. Por ahora uso sólo los 16 colores de la terminal; el color verdadero tendrá sentido cuando haya portadas de dónde tomarlo.
De la fase 3 ya están el gapless y los niveles de calidad con respaldo automático, pero faltan la detección de lo que soporta el DAC (#25), el cambio de frecuencia de muestreo por pista (#26), el indicador de ruta de señal en la TUI (#28), ReplayGain (#30) y el volumen por el mixer del DAC (#31). Las fases 4 y 5 (letras, MPRIS, scrobbling, temas, paquetes para Fedora y Arch, un servidor MCP para agentes) todavía son sólo una línea por issue, salvo la CI. Aún no hay ningún release etiquetado.
Lo que sí me deja es la confirmación de que una idea clara se puede convertir en código que suena, siempre que el plan vaya antes que el código y uno no delegue las decisiones que importan.
El repo es público: github.com/hectmor/phonia. Si tienen un DAC y TIDAL, pruébenlo; si no, lean el DECISIONS.md y díganme en qué me equivoqué. Y si encuentran algo roto, abran un issue.
datzin