Capas independientes, projectM en el mapping y catalogo de la galeria
Mapping - Los clips salian del reves: mapper.js activaba UNPACK_FLIP_Y_WEBGL, pero el modelo va con origen arriba-izquierda y texImage2D ya sube la primera fila en t=0, asi que el flip la mandaba al final. Comprobado renderizando el mapper real en Chromium headless con una fuente mitad roja / mitad azul. - projectM se puede usar YA en una capa: los .milk de data/presets-projectm se traducen a Butterchurn en el navegador al elegirlos (milkdrop-preset- converter). La capa MilkDrop gana 'biblioteca' (butter | projectm) sin cambiar su firma, para no recrear el contexto WebGL al cambiar de una a otra. Medido: 100/100 presets de una muestra convierten, 84/84 de los que llevan shaders warp/comp; ~7 ms por preset. - Cada capa tiene su pestana arriba y su propia vista, y '+ CAPA' crea una y entra en ella: con varias capas, ir y volver a MAPPING no era viable. - Dos capas MilkDrop sin preset ya no salen identicas (cogian el indice 0): cada instancia elige uno al azar y lo escribe en el estado. - Una capa nueva ya no nace en la fuente "motor", que es el lienzo del motor activo y hacia que dos capas ensenaran lo mismo. Galeria de visuales - scripts/catalogar-visuales.py: ficha de cada clip (pelicula, personajes, duracion) y, sobre todo, si es una silueta de verdad y cuanta figura tiene. Distingue silueta de corte crudo por el negro puro del fondo: 0,63-0,90 frente a 0,04-0,06, sin zona gris. - El panel lista los clips agrupados por pelicula, con nombre legible y aviso de los que casi no tienen figura, en vez del nombre del archivo. - Soporte de siluetas con canal alfa (.webm VP9): transparencia de verdad, sin recorte por luminancia. El shader del mapper ya la respeta. Documentacion - docs/visuales.md nuevo; pendiente.md al dia con lo hecho y lo que queda. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5d5223db9f
commit
f017246f0e
30 changed files with 3771 additions and 292 deletions
237
docs/arquitectura.md
Normal file
237
docs/arquitectura.md
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue