# Arquitectura

Esta página es para quien vaya a **tocar el código** (o para uno mismo dentro de
seis meses). Explica cómo está montado FOSFENO, dónde vive cada cosa y cómo se
añade algo nuevo sin romper el resto.

## Las tres piezas

```
   ┌───────────────┐   WebSocket   ┌────────────────┐   WebSocket   ┌──────────────┐
   │  PANEL        │ ────────────▶ │  SERVIDOR      │ ────────────▶ │  ESCENARIO   │
   │  web/panel/   │ ◀──────────── │  backend/      │ ◀──────────── │  web/stage/  │
   │  (el móvil)   │    estado     │  server.py     │    estado     │  (Chromium)  │
   └───────────────┘               └───────┬────────┘               └──────┬───────┘
                                           │ subprocess                    │ HDMI
                                           ▼                               ▼
                                    projectM (nativo)                 [ proyector ]
```

- **Servidor** (`backend/server.py`, Flask + Flask-SocketIO). Guarda **el
  estado**, sirve los archivos web, lanza y vigila projectM, enruta el audio con
  `pactl` y persiste lo que hay que persistir.
- **Panel** (`web/panel/`). La interfaz del móvil. **No calcula nada**: refleja
  el estado y manda órdenes.
- **Escenario** (`web/stage/`). La página que sale por el proyector. Es la que
  **de verdad pinta**: Butterchurn, Hydra, Shaders y el Mezclador viven aquí,
  igual que el detector de BPM y la captura de audio.

Regla de oro: **el estado vive en el servidor**, y todo el mundo lo recibe
entero en cada cambio. Ni el panel ni el escenario guardan verdades propias; si
algo hay que recordar, va al estado.

## El estado

Es un único diccionario en `server.py` (`state`) que se difunde con
`socketio.emit("state", state)` en cada cambio. Sus ramas:

| Rama | Qué guarda |
|------|------------|
| `engine` | motor activo: `projectm`, `butterchurn`, `hydra`, `shaders`, `mixer` |
| `power` | visuales encendidas o en negro |
| `sensitivity` | ganancia aplicada al audio antes del análisis |
| `audio` | fuente (`mic` / `monitor`), tarjeta elegida y BPM detectado |
| `butterchurn` | preset, cambio automático (segundos o compases) y transición |
| `hydra` / `shaders` | el código que se está ejecutando y su etiqueta |
| `mixer` | las dos capas (`source`, `fondo`, `camOn`, `video`), modo de mezcla y efectos |
| `projectm` | categoría, visual, variante, favoritos, monitor, congelado |
| `mapping` | `enabled`, `edit`, `masks[]` y `surfaces[]`, donde cada superficie es una **capa** (geometría + `fuente` + opacidad + mezcla) |
| `meta` | listas detectadas: presets, cámaras, micrófonos, vídeos, monitores |
| `status` / `notifications` / `network` | qué se ve, avisos e IP para el QR |

Lo que sobrevive a un reinicio se guarda en `data/`:
`mapping.json` (mapping) y `pm-favoritos.json` (favoritos de projectM). El resto
del estado es de la sesión.

## Eventos de WebSocket

**Del panel al servidor**

| Evento | Qué hace |
|--------|----------|
| `set_power`, `set_engine`, `set_sensitivity` | los tres mandos básicos |
| `update_settings {engine, patch}` | parchea una rama del estado (`butterchurn`, `mixer`, `audio`, `projectm`) |
| `run_code {engine, code, label}` | ejecuta código Hydra o GLSL |
| `engine_command {action}` | `next`, `prev`, `lock`, `fix_current` |
| `set_mapping {…}` | activar, editar, superficies y máscaras (persiste) |
| `pm_favorito {action…}` | añadir, borrar o lanzar un favorito de projectM |
| `rescan_devices`, `rescan_videos`, `reacquire_audio` | volver a mirar el hardware y la galería |
| `system {action}` | apagar o reiniciar la máquina |

**Del escenario al servidor**

| Evento | Qué hace |
|--------|----------|
| `stage_meta` | publica lo que ha detectado el navegador (micrófonos, cámaras, presets) |
| `stage_status {label, bpm}` | qué se está viendo y a cuántos BPM |
| `stage_notify {level, message}` | manda un aviso a la banda del panel |
| `audio_route {target}` | pide enrutar la captura al monitor del sistema o al micro |
| `set_mapping` | al arrastrar los tiradores sobre las propias visuales |

**Del servidor a los clientes**: `state` (el estado entero), `status` (solo
etiqueta y BPM, más ligero), `notify`, `stage_command`, `stage_rescan`,
`stage_reacquire`.

**Nada de esto pide credenciales por defecto.** Cualquiera que alcance el
puerto puede emitir cualquier evento, y eso incluye apagar la máquina y
ejecutar código en el escenario. Si se configura `auth.token`, el handler de
`connect` rechaza a quien no lo traiga en `auth`, y con eso queda cerrado todo
lo demás de golpe. Ver [Seguridad](seguridad.md).

Lo que entra por el socket **no es de fiar**, así que se filtra antes de tocar
el estado: `update_settings` pasa por una lista blanca de claves por motor
(`CLAVES_AJUSTES`), y `set_mapping` sanea capas y máscaras y limita cuántas
acepta. `mapping.json` se escribe **con un segundo de retardo**: arrastrar un
punto emite ~16 veces por segundo y no queremos escribir la microSD a ese
ritmo.

## Los motores

Cada motor es independiente: si a uno le falta su librería o revienta al
arrancar, **los demás siguen funcionando**. `boot()` en `stage.js` los inicia
uno a uno dentro de su propio `try`.

| Motor | Dónde corre | Lienzo |
|-------|-------------|--------|
| **projectM** | proceso nativo, ventana propia | ninguno (la página se queda en negro debajo) |
| **Butterchurn** | navegador, WebGL | `#butterchurn` |
| **Hydra** | navegador, WebGL | `#hydra` |
| **Shaders** | navegador, WebGL a pelo | `#shaders` |
| **Mezclador** | navegador, **sobre Hydra** | `#hydra` |

`applyState()` decide qué lienzo se ve y apaga los demás. El mezclador no es un
motor aparte: **genera código Hydra** (`buildMixerCode`) a partir de los
ajustes; por eso comparte lienzo con Hydra y nunca coinciden.

Butterchurn tiene una particularidad: puede estar pintando **sin ser el motor
activo**, cuando hace de capa de fondo del mezclador. Esa condición está en un
único sitio, `butterLive()`, y de ella dependen el preset, el cambio automático
y los botones de siguiente/anterior.

### Añadir un motor nuevo

1. `ENGINES` en `server.py` y una rama en `state` con sus ajustes.
2. Un lienzo en `web/stage/index.html` y su rama en `applyState()`.
3. Un botón `.engine` en `web/panel/index.html` y su tarjeta de controles.
4. Mostrar u ocultar esa tarjeta en `render()` de `panel.js`.
5. Una entrada en `data/ayuda.json` para el botón de información.

## Audio y BPM

Toda la cadena de audio está **en el navegador** (`stage.js`):
`getUserMedia` → `GainNode` (la sensibilidad) → `AnalyserNode`. De ahí beben el
detector de ritmo, Butterchurn, Hydra y los shaders.

El detector (`BeatDetector`) mira la energía de graves, marca un pulso cuando
supera su media reciente, agrupa los intervalos entre pulsos por parecido y se
queda con el grupo mayoritario. El resultado se dobla o se divide hasta caer en
70–180 BPM. Se publica en `fosBeat` y llega a los motores como `u_bpm`/`u_beat`
(shaders) o `bpm` (Hydra).

La fuente **monitor** («audio del navegador») merece una nota: Chromium oculta
los monitores de PulseAudio/PipeWire al enumerar dispositivos. El plan B es
capturar la entrada por defecto y pedir al servidor que **mueva ese stream** al
monitor de la salida con `pactl`. Solo funciona con servidor y escenario en la
misma máquina — que es el caso del kiosko y del modo portátil.

## Mapping y capas

Dos módulos, con una división limpia: **`layers.js` produce imágenes** y
**`mapper.js` las coloca**.

```
   layers.js                          mapper.js
   ─────────                          ─────────
   capa 1 → Butterchurn  → canvas ┐
   capa 2 → clip .mp4    → video  ├→ resolver(surface) → textura → malla → #output
   capa 3 → ShaderEngine → canvas ┘                                 (WebGL)
   capa 4 → "motor"      → el lienzo del motor activo
```

**`layers.js`** mantiene una instancia por capa y la pone al día con el estado
(`sync`): crea las nuevas, cambia el preset de las que lo hayan cambiado y
destruye las borradas. Decide también qué se comparte y qué no:

- Con **estado propio** (Butterchurn, shaders): **una instancia por capa**, para
  que dos zonas puedan llevar presets distintos.
- Sin estado (clips, cámara): **compartidas por contenido** con cuenta de usos,
  así el mismo clip en dos capas se reproduce una sola vez.

Cada capa se renderiza en un lienzo de **640×360**: el mapper la estira al
deformarla, y esa resolución es lo que hace viable tener varias a la vez en una
Raspberry.

**`mapper.js`** es el compositor WebGL. Para cada superficie pide su textura al
`resolver` (que es `FosLayers.fuenteDe`), la sube **una sola vez por fotograma
y por clave de fuente**, y la dibuja sobre la malla con una homografía por
celda. Encima aplica opacidad (`uAlpha`) y modo de mezcla (`blendFunc`: normal
o aditivo), y al final pinta las máscaras en negro.

Las superficies se guardan en coordenadas **normalizadas 0..1**, así que un
mapping hecho a 1080p sigue valiendo en otra resolución. El formato completo
está documentado en la cabecera de `mapper.js`. Guía de uso:
[Projection mapping](mapping.md).

Una capa **no puede ser projectM**: es un proceso nativo con su propia ventana,
fuera del alcance de un contexto WebGL del navegador. Su equivalente es la
fuente `butter`, que son los mismos presets de MilkDrop.

## Estructura de archivos

```
FOSFENO/
├── backend/server.py        Estado, WebSocket, procesos, audio, subidas
├── web/panel/               Panel de control (móvil): index.html, panel.js, panel.css
├── web/stage/               Escenario: stage.js (motores + audio), layers.js, mapper.js
├── web/lib/                 Librerías servidas tal cual (butterchurn, hydra, codemirror…)
├── data/                    Contenido y ajustes que persisten (ver abajo)
├── scripts/                 Arranque del kiosko, build de projectM, lib.sh
├── docs/                    Esta documentación
├── install.sh               Instalador multi-distro (Debian, Fedora, Arch, openSUSE)
└── fosfeno                  Lanzador del modo portátil
```

`data/` mezcla dos cosas: **contenido editable** que sí va al repositorio
(`hydra-sketches.json`, `hydra-snippets.json`, `shaders.json`, `ayuda.json`,
`presets-projectm/`) y **estado de cada instalación** que no va
(`mapping.json`, `pm-favoritos.json`, `videos/`) — ver `.gitignore`.

## Criterios que sigue el código

Vale la pena respetarlos al tocar algo:

- **Nunca callar un fallo.** Todo error acaba en la banda de avisos del panel
  con una frase que dice qué hacer, no un volcado técnico. Para eso está
  `report()` en el escenario y `notify()` en el servidor.
- **Que un fallo no se lleve el resto por delante.** Sin cámara, sin micro o sin
  projectM, lo demás sigue proyectando.
- **El estado manda.** Si algo hay que recordar entre clientes, va al estado; no
  se guardan verdades locales en el panel.
- **Textos en castellano y sin jerga.** Tanto la interfaz como los avisos están
  escritos para alguien que no ha leído el código.
- **Nada de recursos huérfanos.** Cada instancia de motor lleva su `destruir()`
  y suelta lo suyo: `requestAnimationFrame`, temporizadores, `MediaStream` (el
  piloto de la cámara) y **el contexto WebGL** (`soltarGL`). El navegador solo
  aguanta ~16 contextos y al pasarse mata el más viejo, que es el del
  compositor de mapping: dejarlos colgando acaba en proyector negro.
- **Cambiar un ajuste no recrea el motor.** En `layers.js`, la firma de una
  fuente decide si la instancia se reaprovecha; los ajustes (preset, cambio
  automático) se aplican en caliente con `aplicar()`.

### Trampa conocida: `hydra.hush()` vacía las fuentes

Al salir del Mezclador hacia otro motor se llama a `hydra.hush()`, y eso
**borra `s0` y `s1`**. Por eso el escenario recuerda en `mixerS1` qué elemento
tenía enganchado y lo vuelve a enlazar (`reengancharMixer`) al volver: sin
eso, ir a Butter y volver dejaba el mezclador sin la capa de encima. Si algún
día se añade otra fuente de Hydra, hay que recordarla igual.
