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:
hacklab 2026-08-02 14:56:01 +02:00
parent 5d5223db9f
commit f017246f0e
30 changed files with 3771 additions and 292 deletions

View file

@ -20,16 +20,32 @@ para consultarse a saltos después.
Cómo correr FOSFENO en un portátil Debian, Ubuntu o Mint, sin Raspberry Pi.
- [Uso del panel](uso.md)
Cómo se maneja desde el móvil, qué hace cada motor de visuales y cómo se
configura cada opción.
Cómo se maneja desde el móvil, las dos pestañas (MOTORES y MAPPING), qué
hace cada motor de visuales y cómo se configura cada opción.
- [El Mezclador VJ](mezclador.md)
Las mezclas a fondo: las dos capas, cómo poner visuales de MilkDrop de
fondo, los modos de mezcla y los efectos de color.
- [La galería de visuales](visuales.md)
El catálogo de clips: cómo se generan las fichas, cómo distingue una silueta
de un trozo de película y por qué unos clips salen marcados como flojos.
- [Projection mapping](mapping.md)
Cómo deformar las visuales para encajarlas en superficies físicas: malla,
máscaras y edición desde el panel arrastrando con el ratón o el dedo.
Cómo deformar las visuales para encajarlas en superficies físicas, y cómo
poner **un visual distinto en cada zona**: capas, fuentes, malla y máscaras.
- [Solución de problemas](problemas.md)
Qué hacer cuando algo no arranca, no se ve o no suena. Incluye cómo leer
los mensajes de error que aparecen en el propio panel.
- [Seguridad](seguridad.md)
Qué asume el diseño, qué pasa si pinchas en una wifi pública y cómo cerrar
el panel con una clave compartida (el QR ya la lleva dentro).
- [Arquitectura](arquitectura.md)
Para quien vaya a tocar el código: las tres piezas, el estado, los eventos
de WebSocket, cómo se añade un motor y los criterios que sigue el proyecto.
Si solo quieres empezar rápido, el archivo `README.md` de la raíz del
proyecto tiene la versión resumida.

237
docs/arquitectura.md Normal file
View 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
70180 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.

View file

@ -6,7 +6,14 @@ Material gráfico que usa la documentación.
- `raspberry-pi-5.jpg` — la placa Raspberry Pi 5. Se usa en `requisitos.md`.
- `milkdrop.jpg` — ejemplo de visuales estilo MilkDrop. Se usa en `uso.md`.
- `hydra.png` — el entorno de Hydra. Se usa en `uso.md`.
- `panel.png` — captura del panel de control. Se usa en `uso.md` y el README.
- `panel.png` — captura del panel de control (pestaña MOTORES). Se usa en
`uso.md` y el README.
- `mezclador.png` — la tarjeta del Mezclador VJ con visuales de fondo. Se usa
en `mezclador.md`.
- `mapping.png` — la pestaña MAPPING con la previsualización. Se usa en
`mapping.md`.
- `capas.png` — tres capas con visuales distintos (dos MilkDrop + un clip +
una franja de shader). Se usa en `mapping.md` y el README.
- `demo.mp4` — vídeo corto de FOSFENO en marcha. Se usa en el README
(recomprimido para que no pese; el original eran 80 MB).

BIN
docs/assets/capas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

BIN
docs/assets/mapping.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
docs/assets/mezclador.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Before After
Before After

View file

@ -36,7 +36,7 @@ a la Raspberry el nombre de red `fosfeno`, y `fosfeno.local` es la forma
estándar de localizar un equipo por su nombre en la red local.
**Escribir la dirección IP.** Es la que aparece en la pantalla de conexión del
proyector, algo como `http://192.168.1.71/`. Funciona siempre, pero la IP puede
proyector, algo como `http://192.168.1.XX/`. Funciona siempre, pero la IP puede
cambiar de un día para otro, así que es la opción de reserva.
## El móvil tiene que estar en la misma red

View file

@ -1,50 +1,164 @@
# Projection mapping
> **Pestañas de capa.** Cada capa tiene su propia pestaña arriba, junto a
> MOTORES y MAPPING, y el botón **+ CAPA** crea una y te deja ya dentro de
> ella. Esa vista enseña solo la previsualización y los ajustes de *esa* capa:
> es la forma de trabajar cuando hay varias, sin ir y volver a la lista.
>
> **Visuales de projectM en una capa.** La fuente "Visuales MilkDrop" tiene
> ahora un selector de **biblioteca**: los ~100 presets de Butterchurn, o los
> **9795 `.milk` de projectM** (categoría / visual / variante, o "uno al
> azar"). Los `.milk` se traducen a Butterchurn en el propio navegador al
> elegirlos. Ver `pendiente.md` punto 2 para el detalle y sus límites.
FOSFENO puede **deformar las visuales** para encajarlas en superficies físicas
(una pared en ángulo, cajas, un objeto, varias zonas de una fachada) en vez de
proyectar un simple rectángulo. Todo se hace **desde el panel**, arrastrando con
el ratón o el dedo, y se **guarda solo**.
Y no es una sola imagen troceada: **cada zona es una capa con su propio
visual**. MilkDrop a la derecha, otro preset distinto a la izquierda y un vídeo
en el centro, cada uno en su sitio y a la vez. Eso está en
[Capas: un visual distinto en cada zona](#capas-un-visual-distinto-en-cada-zona).
Se aplica a los motores que corren en el navegador: **Butterchurn, Hydra,
Shaders y Mezclador**. (projectM nativo aún no; ver [Limitaciones](#limitaciones).)
## Dónde está: la pestaña MAPPING
El mapping tiene **su propia pestaña**, a la derecha del título **FOSFENO**, al
lado de **MOTORES**. Están separados a propósito: MOTORES es el directo
(encendido, audio, motores) y MAPPING es el montaje. Así ni se estorban ni se
confunden.
En la pestaña MAPPING la tarjeta ocupa el ancho entero, para que la
previsualización sea grande y se puedan arrastrar los puntos con comodidad —
también con el dedo, desde el móvil.
Mientras el mapping esté activado, la pestaña lleva un **punto verde**: desde
MOTORES se ve de un vistazo que la salida se está deformando.
<div align="center">
<img src="assets/mapping.png" alt="La pestaña MAPPING del panel" width="720">
</div>
## En 4 pasos
1. Elige un motor que **no** sea projectM (Butterchurn, Hydra, Shaders o Mezcla).
2. En el panel, sección **Mapping**, marca **Activar mapping**.
3. Pulsa **+ Superficie**. Aparece un recuadro con las visuales en la
previsualización del panel (y en el proyector).
4. En la **previsualización** del panel, **arrastra los puntos** para encajar la
forma sobre tu superficie real. Se ve en el proyector al instante.
1. Entra en la pestaña **MAPPING** y marca **Activar mapping**.
2. Pulsa **+ Capa**. Aparece un recuadro en la previsualización del panel (y en
el proyector).
3. **Arrastra los puntos** para encajar la forma sobre tu superficie real. Se ve
en el proyector al instante.
4. Con la capa seleccionada, en **Propiedades** eliges **qué se proyecta ahí**.
## La sección Mapping del panel
## La pestaña MAPPING por dentro
Tiene tres bloques, de arriba abajo: la **salida**, las **capas** y las
**propiedades** de la capa que tengas seleccionada.
**Salida**
- **Activar mapping** — enciende/apaga la deformación.
- **Modo edición** — muestra u oculta los tiradores en la **ventana de las
visuales** (por si prefieres ajustar ahí con un ratón). Para el uso normal no
hace falta: con la previsualización del panel basta.
- **+ Superficie** — añade una superficie nueva (un cuadrilátero).
- **Editar en el escenario** — saca los tiradores y la malla de colores
también sobre las propias visuales (por si prefieres ajustar ahí con un
ratón). Para el uso normal **no hace falta**: con la previsualización del
panel basta, y así el proyector sale limpio. Es un modo de montaje, no un
ajuste: **arranca siempre apagado**, aunque lo dejaras puesto.
- **Previsualización** — el recuadro donde arrastras los puntos (ratón o dedo).
Cada capa lleva escrito su nombre y qué visual tiene dentro.
**Capas**
- **+ Capa** — añade una zona nueva. Sale escalonada, para que no tape a la
anterior, y queda seleccionada.
- **+ Máscara** — añade una **máscara**: un recuadro **negro** que tapa una zona
(útil para recortar el derrame de luz fuera de la superficie física).
- **Reset** — borra todas las superficies y máscaras.
- **Previsualización** — el recuadro donde arrastras los puntos (ratón o dedo).
Muestra una rejilla dentro de cada superficie para que veas cómo se deforma.
- **Lista** — cada superficie y máscara con su tamaño de malla y un botón **✕**
para borrarla.
> **Capa ≠ máscara.** Una **capa** proyecta algo (eliges qué en Propiedades);
> una **máscara** solo tapa y no lleva visual. Es la confusión más fácil: si
> creaste una máscara buscando dónde poner el visual, selecciónala y pulsa
> **Convertirla en capa** — se sustituye por una capa en el mismo sitio y con
> la misma forma.
- **Reset** — borra todas las capas y máscaras.
- La **lista** se lee como en cualquier mesa de VJ: **la de arriba es la que
queda delante** en el proyector. Cada fila tiene:
| Botón | Qué hace |
|-------|----------|
| ◉ / ○ | Enciende o apaga la capa sin borrarla |
| ▲ ▼ | La sube o la baja en el orden de pintado |
| ✕ | La borra |
**Propiedades** — nombre, qué se proyecta, opacidad, mezcla y malla. Se explican
abajo.
## Capas: un visual distinto en cada zona
Esto es lo que separa el mapping de FOSFENO de un simple recorte: **cada capa
elige su propia fuente**, así que la proyección puede llevar cosas distintas a
la vez.
| Fuente | Qué es | Coste |
|--------|--------|-------|
| **El motor de MOTORES** | Lo que esté puesto en la otra pestaña. En varias capas a la vez, todas enseñan lo mismo | ninguno |
| **Visuales MilkDrop propias** | Un Butterchurn **solo para esa capa**, con su preset. Es lo que permite dos looks de MilkDrop distintos a la vez | alto |
| **Shader GLSL** | Uno de los shaders de la librería, reaccionando al audio | medio |
| **Clip o imagen** | Un archivo de `data/videos`. En bucle y sin sonido | bajo |
| **Cámara** | La webcam | bajo |
| **Negro** | Nada: tapa la zona sin usar máscara | ninguno |
Las capas de MilkDrop y de shader **son motores de verdad funcionando**: cada
una se renderiza aparte y el mapper la estira al deformarla. Los clips y la
cámara, en cambio, **se comparten**: si dos capas usan el mismo clip, se
reproduce una sola vez.
### La receta del ejemplo
Tres zonas, tres visuales distintos:
1. **+ Capa** → nómbrala «Pared izquierda» → fuente **Visuales MilkDrop
propias** → elige un preset.
2. **+ Capa** → «Centro» → fuente **Clip o imagen** → elige tu vídeo.
3. **+ Capa** → «Pared derecha» → fuente **Visuales MilkDrop propias** → *otro*
preset, o marca **Cambiar de preset solo** para que vaya rotando.
4. Arrastra cada una a su sitio en la previsualización.
<div align="center">
<img src="assets/capas.png" alt="Tres capas con visuales distintos" width="820">
</div>
### Transiciones
Las capas de **Visuales MilkDrop** llevan su propio deslizador de
**transición (08 s)**: es lo que tarda un preset en disolverse en el
siguiente. A 0 el cambio es un corte seco; subiéndolo, uno se funde con el
otro sin que se note. Combinado con **Cambiar de preset solo**, la capa va
alternando visuales sola y en suave.
El fundido lo hace el propio Butterchurn mientras carga, así que no cuesta
rendimiento aparte.
### Opacidad y mezcla
Cuando dos capas se solapan, mandan estos dos ajustes:
- **Opacidad** — cuánto deja ver lo que hay debajo.
- **Mezcla****Normal** tapa lo de debajo; **Sumar** apila luz, así que las
zonas oscuras de la capa dejan pasar la de abajo. Sumar es lo que da los
solapes bonitos entre visuales; normal es lo que quieres cuando cada capa va
en su pared y no se tocan.
## Superficies con malla (curvar)
Una superficie nueva es un cuadrilátero de 4 esquinas (corrección de
perspectiva / *keystone*). Para superficies **curvas**:
Una capa nueva es un cuadrilátero de 4 esquinas (corrección de perspectiva /
*keystone*). Para superficies **curvas**:
1. Selecciona la superficie (toca dentro de ella en la previsualización).
2. Aparece ** malla / + malla**: cada **+ malla** la subdivide (2×2, 3×3…
hasta 6×6).
1. Selecciona la capa (toca dentro de ella en la previsualización).
2. En Propiedades, **Malla**: cada **+** la subdivide (2×2, 3×3… hasta 6×6).
3. Ahora puedes arrastrar **cualquier punto** de la rejilla (no solo las
esquinas) para curvarla y adaptarla a superficies no planas.
Puedes crear **varias superficies** y mapear la misma imagen en distintas zonas.
## Máscaras
Una máscara es un cuadrilátero **negro** que se pinta encima de todo. Sirve para
@ -60,18 +174,39 @@ cero, usa **Reset** o borra ese archivo.
## Editar en la ventana de las visuales (opcional)
Con **Modo edición** activo, los tiradores también aparecen sobre las propias
visuales. Ahí, además, funcionan atajos de teclado:
Con **Editar en el escenario** activo, los tiradores también aparecen sobre las
propias visuales, con el nombre y la fuente de cada capa escritos encima. Ahí,
además, funcionan atajos de teclado:
- **doble clic** en un hueco: crea una superficie nueva.
- **+ / **: subdivide/reduce la malla de la superficie seleccionada.
- **Supr**: borra la superficie o máscara seleccionada.
- **doble clic** en un hueco: crea una capa nueva.
- **+ / **: subdivide/reduce la malla de la capa seleccionada.
- **Supr**: borra la capa o máscara seleccionada.
## Cuánto aguanta
Cada capa de **MilkDrop** o de **shader** es un motor de verdad renderizando
aparte, así que tienen un coste real:
| Equipo | Capas de MilkDrop / shader | Capas de clip o cámara |
|--------|---------------------------|------------------------|
| Portátil con GPU | 34 sin despeinarse | muchas |
| Raspberry Pi 5 | 2, con margen justo | varias |
| Raspberry Pi 4 | 1 | unas pocas |
Cada capa se renderiza a **640×360** y el mapper la estira al deformarla: en una
zona de la proyección no se nota, y es lo que hace viable tener varias a la vez.
Si va lenta, lo primero que hay que quitar son las capas de MilkDrop de más;
clips y cámara cuestan mucho menos, y además **se comparten** entre capas.
## Limitaciones
- **projectM nativo no se mapea todavía.** projectM corre en su propia ventana
fuera del navegador, así que el mapper (que trabaja sobre las visuales web) no
lo alcanza. Para mapear el mismo tipo de gráficos, usa **Butterchurn**
(MilkDrop en el navegador). El soporte de projectM está en estudio.
- El mapping es una capa de deformación sobre la imagen final; no cambia el
contenido de cada motor.
- **projectM nativo no puede ir en una capa.** projectM corre en su propia
ventana fuera del navegador, así que el mapper no lo alcanza. Para el mismo
tipo de visual, usa la fuente **Visuales MilkDrop propias**: son los mismos
presets de MilkDrop, dentro del navegador, y además puedes tener varios
distintos a la vez. Meter la ventana nativa como capa (capturándola) está en
estudio.
- El mapping deforma y compone; no cambia el contenido de cada motor.
Cómo está montado por dentro (el compositor WebGL, las capas, el formato de las
superficies): [Arquitectura](arquitectura.md#mapping-y-capas).

182
docs/mezclador.md Normal file
View file

@ -0,0 +1,182 @@
# El Mezclador VJ (mezclas)
El **Mezclador** es el motor de vídeo de FOSFENO. No es un motor de visuales
generativas como los otros: es una **mesa de mezclas de dos capas** que combina
en directo una capa de fondo con un clip, y les aplica efectos de color.
Se elige con el botón redondo **Mezcla** del apartado *Motor de visuales*, en la
pestaña **MOTORES**.
## El modelo mental: siempre dos capas
Todo el mezclador se entiende con esta idea. Hay **dos capas y solo dos**, y
siempre son las mismas:
```
┌──────────────────────────────────────────────┐
│ CAPA DE ENCIMA :: el CLIP │ un vídeo o una imagen
│ (data/videos/…) │ de la galería
├──────────────────────────────────────────────┤
│ CAPA DE ABAJO :: el FONDO │ la cámara web
│ (cámara o visuales Butter / MilkDrop) │ o las visuales Butter
└──────────────────────────────────────────────┘
[ modo de mezcla ] → proyector
```
La tarjeta del panel está numerada en ese mismo orden, y ese orden es la
respuesta a casi cualquier duda:
| Paso | Qué decide |
|------|------------|
| **1 · Qué se ve** | si sale solo el fondo, solo el clip, o los dos |
| **2 · Fondo** | qué hay en la capa de abajo: **cámara** o **visuales Butter** |
| **3 · Clip** | qué vídeo o imagen va en la capa de encima |
| **4 · Cómo se juntan** | el modo de mezcla (solo aparece si se ven los dos) |
Debajo del paso 4 hay una **línea naranja de resumen** que dice, en una frase,
lo que va a salir por el proyector con los ajustes que tengas puestos. Si en
algún momento no sabes qué estás montando, esa línea lo dice.
<div align="center">
<img src="assets/mezclador.png" alt="La tarjeta del Mezclador VJ" width="480">
</div>
## Poner visuales de fondo (MilkDrop detrás del vídeo)
Esto es lo que suele costar encontrar, así que va aparte. **Sí está integrado**
y funciona así:
1. Motor **Mezcla**.
2. Paso 2, desplegable **Fondo (capa de abajo)** → **Visuales Butter
(MilkDrop)**.
3. La cámara se apaga sola (piloto incluido) y su sitio en la capa de abajo lo
ocupan las visuales de MilkDrop.
4. Justo debajo aparece el **preset de MilkDrop** que hace de fondo, con
**« Anterior / Siguiente »** y la casilla **Cambiar el fondo solo**.
Ese preset es **el mismo del motor Butter**: lo que elijas aquí queda elegido
también allí, y al revés. El cambio automático también es el mismo (los
segundos o los compases se ajustan en la tarjeta del motor Butter).
> **projectM no puede ir de fondo.** projectM es un programa nativo que corre
> en su propia ventana, fuera del navegador, así que el mezclador no puede
> leerlo como capa. El fondo de visuales es siempre Butterchurn — que es el
> mismo MilkDrop, con los mismos presets, dentro del navegador.
## Receta: un personaje recortado sobre las visuales
Es el uso estrella del mezclador y encadena todo lo anterior:
1. **1 · Qué se ve****Los dos**.
2. **2 · Fondo****Visuales Butter (MilkDrop)**, y elige ahí el preset.
3. **3 · Clip** → un clip de silueta sobre negro (los `_silueta-negro` que
genera DETEKTION, el proyecto hermano de `VISUALES/`, o cualquier vídeo con
fondo negro).
4. **4 · Cómo se juntan****Recorte (personaje delante)**.
El negro del clip se vuelve transparente y la figura queda **delante** de
MilkDrop, latiendo con la música. En este modo el primer deslizador deja de ser
la mezcla A/B y pasa a llamarse **Umbral del recorte**: súbelo si queda un halo
oscuro alrededor de la figura, bájalo si se está comiendo partes del personaje.
## Paso 1 · Qué se ve
- **Solo el fondo** — a pantalla completa. El clip no se usa.
- **Solo el clip** — a pantalla completa. El fondo no se usa (ni la cámara ni
las visuales Butter: si eliges esto, el fondo queda atenuado en el panel).
- **Los dos** — se combinan según el paso 4.
Los controles que en ese momento no pintan nada **no desaparecen: se atenúan**,
para que se vea que existen sin que despisten.
## Paso 2 · Fondo (capa de abajo)
**Cámara.** Con la casilla *Cámara activada* y, si tienes más de una, el
desplegable para elegirla. Si la cámara falla, el aviso sale en la banda de
arriba del panel; para reintentar, apaga y enciende la casilla.
**Visuales Butter (MilkDrop).** Lo explicado más arriba. La cámara se libera de
verdad mientras tanto.
## Paso 3 · Clip (capa de encima)
Los clips salen de la carpeta `data/videos/`. Hay dos formas de meterlos:
- **Copiándolos a la carpeta** y pulsando **Actualizar lista**.
- **Con el botón Subir vídeo o imagen**, desde el móvil o el ordenador, sin
tocar carpetas.
Formatos que van bien:
| Tipo | Formatos | Nota |
|------|----------|------|
| Vídeo | `.mp4` (H.264), `.webm` | En Raspberry, 720p o menos |
| Imagen | `.jpg`, `.png`, `.gif`, `.webp` | Se queda fija como capa |
Los vídeos se reproducen **en bucle y sin sonido** (FOSFENO escucha la música de
la sala, no la del clip).
## Paso 4 · Cómo se juntan
Solo aparece con **Los dos**.
| Modo | Qué hace |
|------|----------|
| **Recorte** | El negro del clip se vuelve transparente: la figura queda **delante** del fondo. El deslizador pasa a ser el umbral |
| **Fundido** | Disuelve una capa sobre la otra según el deslizador de mezcla |
| **Diferencia** | Resta las dos capas: contornos y colores invertidos donde coinciden |
| **Multiplicar** | Oscurece: solo sobrevive lo que es claro en las dos |
| **Sumar** | Aclara: suma la luz de las dos capas |
| **Capa** | Superpone el clip usando su canal alfa |
## Los efectos de color
Se aplican **al resultado ya mezclado**, en este orden (importa: el
caleidoscopio deforma antes de que el color entre en juego):
```
caleidoscopio → rotación → pixelado → tono → saturación → contraste →
brillo → colorama → posterizar → invertir → pulso al ritmo → feedback
```
- **Mezcla A/B** — cuánta capa de encima frente a la de abajo (o el umbral, en
modo Recorte).
- **Tono / Saturación / Contraste / Brillo** — corrección de color de toda la
vida.
- **Colorama** — recicla los colores; en valores altos psicodelia pura.
- **Posterizar** — reduce el número de colores por franjas.
- **Pixelado** — baja la resolución aparente en bloques.
- **Caleidoscopio** — repite la imagen en N sectores en espejo.
- **Rotación** — gira la imagen.
- **Feedback** — realimenta el fotograma anterior: estelas y túneles. Con
valores altos la imagen tarda en limpiarse.
- **Invertir colores** — el negativo.
- **Pulso al ritmo** — la imagen late con los graves detectados (usa el mismo
análisis de audio que alimenta el BPM).
## Por debajo
El mezclador **no es un motor aparte**: genera código [Hydra](https://hydra.ojack.xyz/)
a partir de los controles y lo ejecuta. La cámara entra como fuente `s0` (o el
lienzo de Butterchurn, si el fondo son visuales) y el clip como `s1`. Por eso
el mezclador y el motor Hydra comparten el mismo lienzo y nunca están activos a
la vez.
Como todo lo que corre en el navegador, la salida del mezclador **se puede
mapear**: ver [Projection mapping](mapping.md).
## Si algo no se ve
FOSFENO no se queda callado: el motivo sale en la banda de avisos, arriba del
panel.
| Aviso | Qué hacer |
|-------|-----------|
| «La cámara está apagada» | Marca *Cámara activada* en el paso 2 |
| «No hay vídeo elegido» | Elige uno en el paso 3 |
| «No hay vídeos» | Sube uno, o copia archivos a `data/videos` y pulsa *Actualizar lista* |
| «No se pudo activar la cámara» | Comprueba el cable, pulsa *Buscar dispositivos de nuevo* y apaga/enciende la casilla |
| «No se pudo cargar …» | El formato no lo traga el navegador: pásalo a `.mp4` (H.264) o `.webm` |
Más casos en [Solución de problemas](problemas.md).

162
docs/pendiente.md Normal file
View file

@ -0,0 +1,162 @@
# Pendiente
Lo que sabemos que falta o falla, con el diagnóstico ya hecho para no volver a
investigarlo desde cero. Ordenado por lo que más duele.
---
## 1. Las capas no son de verdad independientes
**Lo que pasa.** Se pueden poner dos capas con visuales MilkDrop, pero acaban
siendo **la misma imagen**. Y no hay forma de decir "en esta zona el Mezclador,
en esta otra Butter", configurando cada una por su cuenta.
Son **tres cosas distintas** mezcladas en el mismo síntoma:
### 1a. Dos capas Butter sin preset elegido salen idénticas — ✅ HECHO
`web/stage/layers.js`, en `crearButter()`, hacía:
```js
let i = Math.max(0, nombres.indexOf(f.preset));
```
Con `preset: ""` (lo que quedaba si la lista de presets aún no había llegado
al panel al crear la capa), `indexOf("")` devuelve `-1` y el `Math.max(0, -1)`
lo dejaba en **0**: todas las capas así arrancaban en el mismo preset.
**Cómo quedó:** sin preset elegido, cada instancia coge uno **al azar** y lo
devuelve en `presetElegido`. `sync()` avisa por `deps.onPreset(id, preset)`,
que `stage.js` (`anotarPresetDeCapa`) escribe en el estado con `set_mapping`,
así que el panel enseña cuál le tocó en vez de dejar el desplegable en blanco.
No recrea la instancia: la firma de una capa butter sigue siendo el tipo a
secas.
### 1b. Las capas puestas en "El motor de MOTORES" comparten imagen — ✅ HECHO
Esa fuente es, literalmente, el lienzo del motor activo. Dos capas así van a
enseñar lo mismo siempre — no es un fallo, es lo que significa. El problema era
que **era la fuente por defecto** de toda capa nueva, así que el primer
resultado que veía cualquiera era "dos capas iguales".
**Cómo quedó:** una capa nueva ya no nace en "motor". `panel.js` tiene
`fuenteDeCapaNueva()`, que la estrena en MilkDrop con preset al azar
(contenido distinto desde el primer clic) y solo cae en "motor" si Butterchurn
todavía no ha dado su lista de presets. Lo usan tanto `+ Capa` como
`Máscara → Capa`.
### 1c. Falta el Mezclador (y Hydra) como fuente de capa — *(el trabajo de verdad)*
Hoy las fuentes son: motor, butter, shader, clip, cámara, negro. **No está el
Mezclador**, así que "una zona con el Mezclador y otra con Butter" solo se
puede hacer vía "motor", y entonces manda el selector global.
Por qué no está: el Mezclador **es** Hydra (genera código Hydra y lo ejecuta), y
Hydra está montado como instancia única global (`makeGlobal: true`, con `s0`,
`s1`, `o0` en el espacio global). Para tener dos mezcladores independientes hay
que instanciar Hydra varias veces con `makeGlobal: false` y usar la API por
instancia (`h.synth.src(...)`), lo que obliga a reescribir `buildMixerCode` para
que no dependa de los nombres globales.
**Coste realista:** medio día, más el gasto de GPU de un Hydra por capa. Antes
de meterse, decidir si compensa frente a la alternativa barata: dejar el
Mezclador solo como motor a pantalla completa y que las capas tiren de
MilkDrop/shader/clip, que es lo que ya funciona.
### 1d. Los dos paneles no están relacionados
MOTORES y MAPPING van cada uno por su lado: el preset que eliges en el motor
Butter y el de una capa MilkDrop son listas distintas, no comparten favoritos,
y lo que tocas en uno no se refleja en el otro.
**Hacia dónde:** una sola biblioteca de presets y una sola lista de favoritos,
usada por el motor Butter, por las capas del mapping y por projectM. Esto se
solapa con el punto 2: es el mismo trabajo de unificación.
---
## 2. projectM en el mapper — ✅ HECHO (camino A)
**Lo que pasaba.** Al elegir projectM no había forma de mapearlo ni de usarlo
en una capa, y ahí está **la biblioteca de verdad**: 9795 presets `.milk` en 11
categorías y 189 visuales. Butterchurn solo trae ~100 propios.
**Por qué no se podía.** projectM es un proceso nativo que pinta en su propia
ventana X11/Wayland. El compositor de mapping es WebGL dentro del navegador y
solo puede usar como textura lo que vive en la página.
**Cómo quedó.** Se descartó capturar la ventana (frágil en Wayland) y compilar
projectM a WebAssembly (un proyecto en sí). Se hizo el **camino A**: traducir
los `.milk` a presets de Butterchurn **en el propio navegador**, al elegirlos.
- `web/lib/milkdrop-preset-converter.min.js` (npm `milkdrop-preset-converter`,
añadido a `web/package.json` y a `install.sh`).
- `layers.js``cargarMilk(ruta)`: pide el `.milk` al backend (ya los servía),
lo convierte y se lo pasa a `viz.loadPreset()`. Cachea **la promesa**, no el
resultado, para que dos capas pidiendo el mismo preset lo conviertan una vez.
- La capa MilkDrop tiene ahora `biblioteca: "butter" | "projectm"`. La firma de
la capa **sigue siendo `'butter'`**, así que cambiar de biblioteca no recrea
la instancia ni gasta un contexto WebGL más.
- El panel tiene selector de biblioteca y, con projectM, los tres niveles
(categoría / visual / variante) más un botón de "uno al azar".
**Medido, no supuesto:**
- **100/100** presets de una muestra repartida por toda la biblioteca se
convierten sin excepción, incluidos **84/84** de los que llevan shaders
`warp_1`/`comp_1` (que eran justo los dudosos).
- Conversión de un `.milk` real en el navegador: **7 ms**.
- Una capa del mapper con `Dancer/Glowsticks/285.milk` renderiza sin un solo
aviso del escenario.
> Cuidado al medir esto: si el `.milk` no se descarga, Butterchurn **sigue
> pintando su preset de reserva**. Una prueba que solo mire "¿hay píxeles
> encendidos?" da OK igual. Por eso la comprobación exige además que el
> escenario no haya soltado ningún aviso. La primera versión de la prueba dio
> un falso OK exactamente por esto.
**Lo que queda de este punto:** la conversión no es perfecta al 100 % en
fidelidad — algún shader complejo puede verse distinto del original en
projectM. Se convierte y se ve, pero si un preset no convence, la salida es
probar otra variante. Y falta unificar los favoritos entre el motor projectM
y las capas (parte del punto 1d).
---
## 3. Los clips salían del revés en el mapping — ✅ ARREGLADO
**Lo que pasaba.** Al poner un vídeo en una capa del mapping, salía volteado
verticalmente (boca abajo).
**Por qué.** `web/stage/mapper.js`, en `initGL()`, hacía
`gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)`. Pero el modelo del mapper va
con el origen **arriba-izquierda** (`vUV.y = 0` es el borde de arriba de la
superficie) y `texImage2D` ya sube la primera fila de la imagen en `t = 0`.
Con el flip puesto, `t = 0` pasaba a ser la ÚLTIMA fila: todo del revés.
Se notaba con los clips porque una visual de MilkDrop es casi simétrica y no
canta.
**Comprobado**, no deducido: con una fuente mitad roja arriba / mitad azul
abajo, renderizando el `mapper.js` real en Chromium headless y leyendo el
píxel de arriba con `readPixels`, antes salía AZUL y ahora sale ROJO.
De paso se midió el **Mezclador** (Hydra, `s1.init({src})`) con la misma
prueba: ese sale bien, no hay que tocarlo. Si alguna vez se ve del revés ahí,
no es este fallo.
> Al medir esto, ojo con `readPixels` sobre un lienzo sin
> `preserveDrawingBuffer`: devuelve negro y es facilísimo leerlo como "está
> volteado". Hay que reservar el contexto con `preserveDrawingBuffer: true`
> ANTES de que la librería llame a `getContext`.
---
## 4. Menor: no hay fundido al cambiar de motor
Pasar de Mezcla a Butter es un corte seco. Dentro de una capa MilkDrop sí hay
transición suave (deslizador de 08 s), pero cruzar **dos motores** exige
renderizar los dos a la vez durante el cruce y fundirlos en el compositor.
Se puede hacer en el mapper (dibujar la capa dos veces, con la fuente vieja
bajando de alfa y la nueva subiendo), pero cuesta tener los dos motores vivos a
la vez unos segundos. Decidir si merece la pena.

94
docs/seguridad.md Normal file
View file

@ -0,0 +1,94 @@
# Seguridad
FOSFENO es un **aparato de directo**, no un servicio en internet. Está pensado
para una red local: tú, tu móvil y el proyector. Esta página cuenta qué asume
el diseño, qué pasa si la red no es de fiar, y cómo cerrarlo.
## El modelo de amenaza, en una frase
Cualquiera que alcance `http://<ip-de-fosfeno>/` **manda sobre las visuales**.
Por defecto no hay contraseña: es lo cómodo para casa, un ensayo o una red
tuya, y es como funcionó desde el principio.
Eso está bien en tu salón. En **la wifi de un bar o de un festival** significa
que un desconocido puede:
- Apagar o reiniciar el equipo en mitad de la sesión.
- Cambiar de motor, de preset y de mapping.
- **Ejecutar código** en el escenario, porque el editor de Hydra/GLSL es
precisamente eso, y el navegador del kiosko arranca con permiso automático de
**cámara y micrófono**.
- Llenarte la tarjeta subiendo archivos a la galería.
No es un fallo escondido: es la consecuencia de no pedir credenciales. La
solución está abajo y son dos líneas de configuración.
## La clave compartida
En `config.json`:
```json
"auth": {
"token": "loquesea-largo-y-tuyo"
}
```
O sin tocar el archivo, por variable de entorno:
```bash
FOSFENO_TOKEN=loquesea-largo-y-tuyo ./fosfeno
```
Con la clave puesta:
- **El código QR del proyector ya la lleva dentro.** Escaneas y entras, igual
que siempre. No hay que teclear nada.
- Quien solo conozca la IP **no se conecta**: el WebSocket rechaza la conexión,
así que no puede ni mirar el estado ni mandar órdenes.
- Las subidas a la galería también la piden.
- El escenario la recibe del propio servidor al abrirse. No hay nada que
configurar en el navegador.
La contrapartida: si escribes la dirección a mano tendrás que añadir
`?k=tu-clave` al final. Por eso el QR es el camino cómodo.
> Con `token` vacío (el valor de fábrica) todo funciona exactamente como antes.
> Actívala cuando pinches fuera de casa.
## Lo que ya está cerrado
Estas no dependen de que actives nada:
| | |
|---|---|
| **Path traversal** | Las rutas de archivos (`/panel`, `/stage`, `/lib`, `/data`, `/pm/variantes`) resuelven y comprueban que no salen de su carpeta. Probado con `../`, `%2e%2e%2f`, doble codificación y rutas absolutas |
| **Nombres de archivo subidos** | Pasan por `secure_filename` y una lista blanca de extensiones. No se puede escribir fuera de `data/videos` ni subir un `.sh` |
| **Inyección de comandos** | Ningún `shell=True`, ningún `os.system`. Todos los procesos se lanzan con lista de argumentos |
| **Rutas de presets de projectM** | La categoría y el visual vienen del panel, así que se validan contra la carpeta de presets antes de usarse |
| **XSS en el panel** | Los nombres de clips, capas y favoritos se pintan con `textContent`, nunca con `innerHTML` |
| **CORS** | El WebSocket solo acepta su **mismo origen**. Antes admitía cualquiera, así que una web abierta en el móvil de alguien de la sala podía mandar órdenes |
| **Tamaño de subida** | 512 MB por archivo (antes 4 GB, suficiente para llenar una microSD en tres peticiones) |
| **Payloads del WebSocket** | Los ajustes se filtran por lista blanca de claves y las capas del mapping se sanean antes de guardarse, así que no se puede meter basura en el estado ni en `data/mapping.json` |
## Lo que NO deberías hacer
- **No lo expongas a internet.** Nada de abrir el puerto 80 en el router ni
ponerle un túnel. No está pensado para eso y la clave compartida no es
autenticación seria.
- **No dejes el apagado remoto a mano de una red pública** sin clave. El
instalador da permiso `sudo` sin contraseña para `reboot` y `poweroff`
(`install.sh`), que es lo que hace que el botón del panel funcione.
- **No pongas datos personales en `data/videos`.** Cualquiera que entre al
panel puede listarlos y reproducirlos.
## Lo que no se sube al repositorio
`.gitignore` deja fuera lo de cada instalación: `.venv/`, `*.log` (el log del
servidor lleva IPs), `data/mapping.json`, `data/pm-favoritos.json`,
`data/videos/*` y `data/presets-projectm/`. En el repo solo van código,
documentación y los presets de fábrica.
## Si encuentras algo
Es un proyecto pequeño de un hacklab: abre una incidencia en el Gitea del
proyecto contando qué has visto y cómo reproducirlo.

View file

@ -18,6 +18,25 @@ conectado con la Raspberry. Rojo quiere decir que se ha perdido la conexión.
![Panel de control de FOSFENO](assets/panel.png)
## Las dos pestañas: MOTORES y MAPPING
A la derecha del título **FOSFENO** hay dos pestañas. Separan las dos cosas que
se hacen con el panel, que no tienen nada que ver entre sí:
- **MOTORES** — el directo. Encendido, audio, sensibilidad, el motor de
visuales y sus controles. Es la pestaña de siempre y la que está abierta al
arrancar.
- **MAPPING** — el montaje. Encajar la imagen sobre la superficie física:
superficies, malla y máscaras, con una previsualización grande para arrastrar
los puntos.
Lo normal es pasar por MAPPING una vez al montar y quedarse en MOTORES el resto
de la noche. Si el mapping está activado, aparece un **punto verde** en su
pestaña, para que se sepa que la salida se está deformando aunque estés mirando
MOTORES.
El panel recuerda en qué pestaña lo dejaste.
## Avisos y errores
Justo debajo de la cabecera aparecen los avisos. Si algo va mal (la cámara no
@ -56,9 +75,10 @@ librería de fragmentos listos para usar; eliges uno y se carga en el editor.
de código. Los shaders reciben información del audio y del ritmo, así que se
mueven con la música.
**Mezclador.** El modo de vídeo. Mezcla la imagen de una webcam USB con clips
de vídeo y efectos de color. Es lo más parecido a un programa de VJ como
Resolume, pero funcionando dentro de la Raspberry.
**Mezclador.** El modo de vídeo. Monta dos capas —un fondo (la webcam o las
propias visuales de MilkDrop) y un clip encima— y las combina con efectos de
color. Es lo más parecido a un programa de VJ como Resolume, pero funcionando
dentro de la Raspberry.
## Audio y BPM
@ -92,15 +112,23 @@ código es GLSL y tienes los uniforms `u_time`, `u_bass`, `u_mid`, `u_treble`,
## El modo Mezclador
Primero copia tus clips de vídeo en la carpeta `data/videos` del proyecto.
Aparecen solos en el desplegable de vídeo del panel. Para que vayan finos en
la Raspberry conviene que sean clips cortos, en 720p o menos y en H.264.
El mezclador monta **dos capas**: un **fondo** (la cámara web, o las visuales
Butter/MilkDrop) y un **clip** encima (un vídeo o una imagen de la galería). La
tarjeta del panel va numerada en ese orden: qué se ve, el fondo, el clip y cómo
se juntan. Debajo hay una línea de resumen que dice, en una frase, lo que va a
salir por el proyector.
En el panel eliges la fuente: solo la cámara, solo el vídeo, o la mezcla de
las dos. Debajo tienes el modo de mezcla y una fila de controles de color:
tono, saturación, contraste, brillo, colorama, posterizado, pixelado,
caleidoscopio, rotación y feedback. La casilla de pulso al ritmo hace que la
imagen lata con los graves.
Para **poner visuales de fondo**, en el paso 2 cambia el desplegable de *Cámara*
a **Visuales Butter (MilkDrop)**: la cámara se apaga y su sitio lo ocupan las
visuales, con su propio selector de preset ahí mismo. Combinado con un clip de
silueta y el modo *Recorte*, el personaje queda delante de MilkDrop.
Los clips se copian a `data/videos` o se suben desde el propio panel con el
botón *Subir vídeo o imagen*. Para que vayan finos en la Raspberry conviene que
sean cortos, en 720p o menos y en H.264.
La guía completa, con los modos de mezcla, los efectos de color y las recetas:
[El Mezclador VJ](mezclador.md).
## Apagar y reiniciar

160
docs/visuales.md Normal file
View file

@ -0,0 +1,160 @@
# La galería de visuales
Los clips que se ven en el Mezclador y en las capas del mapping viven en
`data/videos`. Además del archivo suelto, hay un **catálogo**
(`data/visuales.json`) con la ficha de cada uno: de qué película sale, quién
aparece, cuánto dura y — lo importante — **si es una silueta de verdad y cómo
de llena está**.
Sin esa ficha el panel solo puede enseñar `p3-davy-jones-tormenta_silueta-negro.mp4`.
Con ella enseña *Davy Jones (en la tormenta)*, agrupado bajo *Piratas del
Caribe: En el fin del mundo*, y marca los que casi no tienen figura.
---
## Cómo se genera
```bash
cd ~/COFRE/CODERS/MUSIKA/VISUALES/FOSFENO
python3 scripts/catalogar-visuales.py # cataloga y escribe el JSON
python3 scripts/catalogar-visuales.py --limpiar # + aparta los que no son siluetas
```
Necesita `ffmpeg`/`ffprobe` y `numpy`. Tarda unos segundos por clip: decodifica
2 fotogramas por segundo a 160x90 en gris y saca las cuentas de ahí.
Hay que volver a pasarlo **cada vez que entran clips nuevos** en la galería. El
panel lo recarga solo al pulsar el botón de recargar del Mezclador, o al subir
un archivo; si el JSON no está, el panel sigue funcionando con los nombres de
archivo pelados.
## Qué mide, y por qué así
**¿Es una silueta o es un trozo de película tal cual?** En un clip siluetado
por DETEKTION el fondo es negro **puro** (valor 0), cosa que no pasa nunca en
una imagen de cine, por oscura que sea. Así que basta con medir qué porcentaje
del cuadro está a cero:
| | negro puro |
|---|---|
| Clips siluetados de la galería | 0,63 0,90 |
| Cortes sin siluetear | 0,04 0,06 |
No hay zona gris, y por eso el umbral está en 0,30. No es un número inventado:
sale de medir la galería entera.
**¿Tiene figura suficiente?** Un clip puede estar perfectamente siluetado y aun
así no servir, porque la escena era muy oscura y el recorte salió casi vacío.
Se mide el porcentaje del cuadro por encima de gris 32, y se mira la **mediana**
(no un fotograma suelto: casi todos los clips tienen tramos vacíos y tramos
llenos, y juzgar por el fotograma central engaña).
| calidad | criterio | qué significa |
|---|---|---|
| `buena` | mediana ≥ 0,03 | se ve bien sobre el fondo |
| `floja` | mediana < 0,03 | figura pequeña o muy oscura |
| `vacia` | mediana < 0,005 o más de la mitad de fotogramas vacíos | apenas hay nada |
| `descartable` | negro puro < 0,30 | no es una silueta: se ve el fondo |
## Qué hace `--limpiar`
Los `descartable` (los que enseñan el fondo entero de la película) se **mueven**
a `data/videos/_descartados/`. No se borran: si un día quieres volver a pasarlos
por DETEKTION, siguen ahí. El servidor no los sirve, porque solo lista archivos
sueltos de `data/videos`, no subcarpetas.
Los `floja` y `vacia` **no se tocan**: son siluetas correctas, solo que oscuras.
Se quedan en la galería marcadas, y ya decides tú si las usas — sobre un fondo
de MilkDrop brillante, una silueta tenue puede funcionar.
## La ficha
```json
{
"archivo": "p3-davy-jones-tormenta_silueta-negro.mp4",
"obra": "Piratas del Caribe: En el fin del mundo",
"anio": 2007,
"titulo": "Davy Jones (en la tormenta)",
"tipo": "silueta",
"modo": "silueta sobre negro",
"origen": "detektion",
"personajes": ["Davy Jones"],
"etiquetas": ["en la tormenta"],
"ancho": 1152, "alto": 480, "fps": 23.98, "duracion": 30.03,
"metricas": { "negroPuro": 0.737, "figura": 0.0582,
"figuraPico": 0.1974, "brillo": 8.49, "vacios": 0.03 },
"clase": "silueta", "calidad": "buena", "aviso": "",
"miniatura": "miniaturas/p3-davy-jones-tormenta_silueta-negro.jpg"
}
```
La miniatura no es un fotograma cualquiera: es **el fotograma con más figura**
de todo el clip, que es el que dice de verdad qué vas a ver.
### De dónde salen los nombres
El catálogo lee las dos convenciones de DETEKTION:
- `siluetear.py``<algo>_silueta-<modo>.mp4``tipo: silueta`
- `recortar.py``<peli>_13m26s.mp4``tipo: corte` (crudo, **sin** siluetear)
y del resto del nombre saca la película (`p1``p5`, `hackers`, `tron`…), los
personajes conocidos y las palabras de escena. Las tablas están arriba del
todo en `scripts/catalogar-visuales.py`: **para añadir películas o personajes
nuevos se tocan ahí y ya**.
Un clip que no encaje en ninguna tabla no rompe nada: sale con el nombre
prettificado y agrupado en "Sin catalogar".
## Transparencia de verdad (canal alfa)
Hay **dos formas** de que un personaje se vea sobre las visuales, y conviene
no confundirlas:
**1. Recorte por luminancia (lo de siempre).** El clip `_silueta-negro.mp4`
tiene fondo negro opaco, y es el Mezclador el que lo hace transparente al
vuelo: `src(s0).layer(src(s1).luma(umbral, 0.15))`. Funciona y va en H.264,
que es lo que mejor decodifica una Raspberry. Pega: hay un umbral que ajustar,
y las zonas oscuras del propio personaje (pelo, ropa negra) se comen con él.
**2. Canal alfa de verdad (`_silueta-alfa.webm`).** El clip lleva la
transparencia dentro, en un WebM con VP9. No hay umbral, no hay bordes
comidos: los píxeles del fondo simplemente **no existen**, y debajo se ve la
capa que haya. Se genera con:
```bash
.venv/bin/python3 siluetear.py clip.mp4 --modo alfa
```
FOSFENO ya lo compone bien sin tocar nada: el fragment shader del mapper
saca `gl_FragColor = vec4(c.rgb, c.a * uAlpha)` y mezcla con
`SRC_ALPHA / ONE_MINUS_SRC_ALPHA`, así que una capa con alfa deja ver la capa
dibujada antes que ella. **Comprobado**, no supuesto: con una capa de fondo
roja y encima un clip alfa, el 48,6 % de la pantalla sale roja por los huecos.
> Para que se vea a través hace falta que **haya algo debajo**: otra capa del
> mapping dibujada antes (MilkDrop, shader, cámara). Si el clip alfa es la
> única capa, debajo solo está el negro del compositor.
### Dos trampas del alfa en WebM
- **El decodificador VP9 nativo de ffmpeg se come el alfa en silencio**: no
falla, devuelve el vídeo entero opaco. Para leerlo hay que pedir
`-c:v libvpx-vp9` a mano. Es exactamente por esto que el catalogador mide
estos clips con `ffmpeg -c:v libvpx-vp9 ... -vf format=rgba,alphaextract`
(y el `format=rgba` no es adorno: sin él `alphaextract` no negocia formato
y ffmpeg aborta).
- **VP9 no tiene decodificación por hardware en la Raspberry Pi 5**, que sí la
tiene para H.264/HEVC. En el portátil va sobrado; en la Pi, con clips de
480p cortos debería ir, pero **hay que probarlo antes de un bolo**. Si se
atraganta, el camino 1 (negro + luma key) sigue ahí.
## En el panel
`server.py` carga el catálogo en `state.meta.visuales` (`{archivo: ficha}`),
filtrado contra lo que hay de verdad en disco para que un catálogo viejo no
enseñe clips borrados. El panel lo usa en `fillClipSelect()`: las listas de
clips salen agrupadas por película con `<optgroup>`, con el título legible, la
duración y la marca de calidad (`· poca figura`, `· casi vacio`), y el aviso
completo en el `title` de cada opción.