FOSFENO: motor de visuales audio-reactivas para Raspberry Pi

Primera version. Cinco motores (projectM, Butterchurn, Hydra, Shaders GLSL y mezclador VJ con camara y video), panel de control web, deteccion de BPM propia, pantalla de conexion con codigo QR, instalador robusto para Raspberry Pi 4 y 5 y documentacion completa en docs/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hacklab 2026-05-22 14:18:19 +02:00
commit 30a09fdee6
31 changed files with 3478 additions and 0 deletions

86
data/ayuda.json Normal file
View file

@ -0,0 +1,86 @@
{
"_info": "Textos de ayuda que muestra el panel al pulsar los botones de informacion.",
"ayuda": {
"power": {
"title": "Encendido de las visuales",
"body": [
"Pone en marcha o detiene las visuales que salen por el proyector.",
"Apagar deja la pantalla en negro pero NO apaga la Raspberry. Es util para hacer una pausa sin tener que reiniciar nada.",
"Para apagar la Raspberry de verdad, usa los botones de Reiniciar y Apagar del final del panel."
]
},
"engines": {
"title": "Motor de visuales",
"body": [
"FOSFENO tiene cinco motores de visuales distintos. Solo uno funciona a la vez.",
"Cada boton redondo cambia al motor correspondiente. Al cambiar de motor aparecen debajo sus propios controles.",
"Pulsa el boton de informacion de cada motor para saber que hace, que necesita y como se configura."
]
},
"projectm": {
"title": "Motor projectM",
"body": [
"Que es: el visualizador clasico de MilkDrop, compilado dentro de la Raspberry. Reacciona al audio por si solo y va rotando entre miles de presets.",
"Requisitos: se compila durante la instalacion. Si la compilacion fallo, este motor sale como no disponible y hay que volver a compilarlo. Los otros cuatro motores funcionan sin el.",
"Como se configura: projectM es un programa nativo, su ventana se pone por encima de las demas visuales. Los botones Anterior y Siguiente cambian de preset, pero eso solo funciona en sesion X11; en Wayland projectM rota presets automaticamente."
]
},
"butterchurn": {
"title": "Motor Butterchurn",
"body": [
"Que es: MilkDrop reescrito para el navegador, con los mismos miles de presets. Reacciona al audio.",
"Requisitos: ninguno especial, se descarga durante la instalacion.",
"Como se configura: puedes elegir un preset concreto en la lista, o activar el cambio automatico. El cambio automatico puede ir por segundos o sincronizado al compas de la musica. El control de transicion ajusta cuanto dura el fundido entre un preset y el siguiente."
]
},
"hydra": {
"title": "Motor Hydra",
"body": [
"Que es: visuales generadas por codigo. Hydra es un lenguaje para escribir visuales en vivo.",
"Requisitos: ninguno especial, se descarga durante la instalacion.",
"Como se configura: el editor de codigo te deja escribir o pegar codigo de Hydra y ejecutarlo al momento. La libreria trae fragmentos listos para usar. En el codigo tienes disponibles time, los valores de audio a.fft[0] a a.fft[4] y la variable bpm."
]
},
"shaders": {
"title": "Motor Shaders",
"body": [
"Que es: shaders GLSL, el tipo de visual de Shadertoy. Programas que dibujan cada pixel de la pantalla.",
"Requisitos: ninguno especial. Los shaders pesados cargan la GPU; si van a tirones, usa un disipador o cambia a un motor mas ligero.",
"Como se configura: el editor de codigo te deja escribir o pegar shaders GLSL. Tienes los uniforms u_time, u_bass, u_mid, u_treble, u_level, u_bpm, u_beat y la textura u_fft con el espectro de audio."
]
},
"mixer": {
"title": "Motor Mezclador VJ",
"body": [
"Que es: el modo de video. Mezcla la imagen de una camara web con clips de video y efectos de color. Es lo mas parecido a un programa de VJ como Resolume.",
"Requisitos: una webcam USB que cumpla el estandar UVC, conectada antes de encender la Raspberry. Para los clips, copia tus archivos de video en la carpeta data/videos del proyecto.",
"Como se configura: elige la fuente (camara, video o mezcla de las dos), el modo de mezcla y los efectos de color con los controles deslizantes. La casilla de pulso al ritmo hace que la imagen lata con los graves.",
"Si tienes mas de una camara, eligela en la lista de camaras del panel. El boton Actualizar lista de videos vuelve a leer la carpeta data/videos por si has copiado clips nuevos."
]
},
"audio": {
"title": "Audio y BPM",
"body": [
"Que es: aqui se elige por que entrada escucha FOSFENO la musica, y se ve el BPM que detecta.",
"Requisitos: un microfono USB o una tarjeta de sonido USB con entrada. La Raspberry no tiene entrada de audio propia.",
"Como se configura: por defecto FOSFENO coge el microfono USB automaticamente. Si tienes varias entradas, eligela en la lista. El BPM se calcula solo a partir del sonido y tarda unos segundos en estabilizarse.",
"Si conectas el microfono o la camara con la Raspberry ya encendida, pulsa el boton Buscar dispositivos de nuevo y apareceran en sus listas sin tener que reiniciar."
]
},
"sensibilidad": {
"title": "Sensibilidad al audio",
"body": [
"Que es: ajusta cuanto reaccionan las visuales al volumen de la musica.",
"Como se configura: si la sala suena floja y las visuales se quedan quietas, sube la sensibilidad. Si todo se ve saturado y exagerado, bajala. El valor 1 es el punto de partida normal."
]
},
"editor": {
"title": "Editor de codigo",
"body": [
"Que es: un editor para escribir, pegar y ejecutar codigo de visuales en vivo. Aparece en los motores Hydra y Shaders.",
"Como se configura: elige un ejemplo de la libreria y se carga en el editor, listo para ejecutarse. Puedes modificarlo o pegar codigo tuyo. El boton Ejecutar lanza lo que haya en el editor. El boton Limpiar lo vacia.",
"Si el codigo tiene un error, FOSFENO lo avisa en la parte de arriba del panel y el detalle tecnico queda en la consola del navegador."
]
}
}
}

22
data/hydra-sketches.json Normal file
View file

@ -0,0 +1,22 @@
{
"kaleido": {
"name": "Caleidoscopio",
"code": "osc(10, 0.1, () => 1 + a.fft[0]*2).color(0.9,0.2,0.8).rotate(() => time*0.1).kaleid(() => 3 + Math.round(a.fft[2]*6)).modulate(noise(() => 1 + a.fft[1]*3), 0.3).out(o0)"
},
"tunel": {
"name": "Tunel",
"code": "shape(99, 0.15, 0.5).repeat(() => 2 + a.fft[0]*5, () => 2 + a.fft[1]*5).modulateScale(osc(4), () => a.fft[2]).scale(() => 1 + a.fft[0]).color(0.2,0.8,1.0).out(o0)"
},
"plasma": {
"name": "Plasma",
"code": "osc(6, 0, () => a.fft[0]*4).modulate(osc(6).rotate(1.2), 0.4).color(() => 0.5+a.fft[1], 0.3, () => 0.8+a.fft[2]).saturate(2).out(o0)"
},
"celdas": {
"name": "Celdas",
"code": "voronoi(() => 4 + a.fft[0]*22, 0.3, 0.2).color(1.0,0.3,0.6).modulatePixelate(noise(3), 0.2).rotate(() => time*0.05).out(o0)"
},
"estrellas": {
"name": "Estrellas",
"code": "noise(() => 3 + a.fft[1]*6, 0.15).thresh(() => 0.62 - a.fft[0]*0.45).color(0.7,0.9,1.0).modulateRotate(osc(2), 0.2).out(o0)"
}
}

55
data/hydra-snippets.json Normal file
View file

@ -0,0 +1,55 @@
{
"_info": "Libreria de fragmentos de codigo Hydra para el editor de FOSFENO. Adaptados de ejemplos de la comunidad de Hydra (github.com/hydra-synth/hydra y github.com/zachkrall/hydra-examples). Edita o anade los tuyos libremente.",
"snippets": {
"osc-basico": {
"name": "Oscilador basico",
"author": "Hydra (comunidad)",
"code": "osc(20, 0.1, 0.8)\n .out(o0)"
},
"kaleido-audio": {
"name": "Caleidoscopio reactivo",
"author": "Hydra (comunidad)",
"code": "osc(15, 0.1, 1)\n .kaleid(() => 3 + a.fft[0]*8)\n .color(1, 0.4, 0.8)\n .rotate(() => time*0.1)\n .out(o0)"
},
"feedback": {
"name": "Feedback infinito",
"author": "Hydra (comunidad)",
"code": "osc(4, 0.1, 1.2)\n .modulate(o0, () => 0.4 + a.fft[0]*0.5)\n .color(0.9, 0.3, 0.6)\n .out(o0)"
},
"voronoi-liquido": {
"name": "Voronoi liquido",
"author": "Hydra (comunidad)",
"code": "voronoi(8, 0.3, 0.2)\n .modulate(osc(5).rotate(0.7), 0.4)\n .color(0.2, 0.8, 1.0)\n .out(o0)"
},
"lluvia-pixel": {
"name": "Lluvia de pixeles",
"author": "Hydra (comunidad)",
"code": "noise(() => 4 + a.fft[1]*8, 0.1)\n .thresh(0.7)\n .modulate(noise(2).scrollY(0, 0.2))\n .color(0.6, 0.9, 1.0)\n .out(o0)"
},
"tunel": {
"name": "Tunel pulsante",
"author": "Hydra (comunidad)",
"code": "shape(99, 0.0001, 0.5)\n .repeat(() => 3 + a.fft[0]*3, () => 3 + a.fft[0]*3)\n .modulateScale(osc(4), () => 0.2 + a.fft[2])\n .color(1.0, 0.3, 0.7)\n .out(o0)"
},
"osc-modulado": {
"name": "Oscilador modulado",
"author": "Hydra (comunidad)",
"code": "osc(40, 0.1, () => a.fft[0]*3)\n .modulate(osc(10).rotate(() => time*0.2), 0.5)\n .saturate(1.6)\n .out(o0)"
},
"triangulos": {
"name": "Triangulos al ritmo",
"author": "Hydra (comunidad)",
"code": "shape(3, () => 0.2 + a.fft[0]*0.4, 0.1)\n .repeat(5, 5)\n .rotate(() => time*0.1)\n .color(0.1, 0.9, 0.8)\n .out(o0)"
},
"espiral": {
"name": "Espiral cromatica",
"author": "Hydra (comunidad)",
"code": "osc(10, 0.05, 1)\n .kaleid(() => 2 + a.fft[1]*10)\n .scale(() => 1 + a.fft[0])\n .rotate(() => time*0.3)\n .colorama(() => a.fft[2]*0.5)\n .out(o0)"
},
"glitch-rgb": {
"name": "Glitch RGB",
"author": "Hydra (comunidad)",
"code": "osc(30, 0, 1)\n .modulate(noise(() => 2 + a.fft[0]*6), 0.3)\n .color(2.0, 0.5, 0.5)\n .modulateRotate(osc(2), 0.1)\n .out(o0)"
}
}
}

18
data/shaders.json Normal file
View file

@ -0,0 +1,18 @@
{
"plasma": {
"name": "Plasma neon",
"code": "void main(){\n vec2 uv = (gl_FragCoord.xy*2.0 - u_resolution)/u_resolution.y;\n float t = u_time*0.4;\n float v = sin(uv.x*4.0 + t) + sin(uv.y*4.0 - t)\n + sin((uv.x+uv.y)*4.0 + t)\n + sin(length(uv)*8.0 - t*2.0 - u_bass*6.0);\n v += u_treble*4.0;\n vec3 col = 0.5 + 0.5*cos(vec3(0.0,2.0,4.0) + v + u_mid*3.0);\n col *= 0.6 + u_level*1.4;\n gl_FragColor = vec4(col,1.0);\n}"
},
"tunel": {
"name": "Tunel infinito",
"code": "void main(){\n vec2 uv = (gl_FragCoord.xy*2.0 - u_resolution)/u_resolution.y;\n float a = atan(uv.y,uv.x);\n float r = length(uv);\n float d = 0.3/r + u_time*0.6 + u_bass*1.8;\n float rings = sin(d*10.0)*0.5+0.5;\n float spokes = sin(a*8.0 + u_time)*0.5+0.5;\n vec3 col = mix(vec3(0.04,0.0,0.18), vec3(0.0,1.0,0.9), rings*spokes);\n col += vec3(1.0,0.2,0.6)*u_treble*spokes*2.0;\n col *= smoothstep(0.0,0.45,r);\n gl_FragColor = vec4(col,1.0);\n}"
},
"rejilla": {
"name": "Rejilla cyber",
"code": "void main(){\n vec2 uv = (gl_FragCoord.xy*2.0 - u_resolution)/u_resolution.y;\n uv *= 1.0 + u_bass*0.9;\n uv.y += u_time*0.2;\n vec2 g = abs(fract(uv*4.0) - 0.5);\n float line = smoothstep(0.46,0.5, max(g.x,g.y));\n vec3 base = vec3(0.0,0.9,1.0) + vec3(u_mid*1.5);\n vec3 col = mix(vec3(0.02,0.0,0.06), base, 1.0-line);\n col += vec3(1.0,0.1,0.8)*u_treble*(1.0-line)*2.0;\n gl_FragColor = vec4(col,1.0);\n}"
},
"espectro": {
"name": "Espectro de barras",
"code": "void main(){\n vec2 uv = gl_FragCoord.xy/u_resolution;\n float f = texture2D(u_fft, vec2(uv.x, 0.5)).r;\n float bar = step(uv.y, f);\n float crest = smoothstep(0.03, 0.0, abs(uv.y - f));\n vec3 col = mix(vec3(0.4,0.0,0.8), vec3(0.0,1.0,0.7), uv.x) * bar;\n col += vec3(1.0,1.0,1.0) * crest;\n gl_FragColor = vec4(col, 1.0);\n}"
}
}

17
data/videos/README.txt Normal file
View file

@ -0,0 +1,17 @@
FOSFENO :: carpeta de videos
============================
Copia aqui tus clips de video para el modo "Mezclador VJ".
- Formatos: .mp4 (recomendado), .webm, .mov, .m4v, .ogv
- Apareceran automaticamente en el panel de control.
- Para mejor rendimiento en la Raspberry Pi usa clips:
* en 720p o menos
* codificados en H.264
* de corta duracion (loops)
Ejemplo desde linea de comandos para reescalar un video pesado:
ffmpeg -i original.mp4 -vf scale=-2:720 -c:v libx264 -an clip.mp4
Los videos NO se suben al repositorio (estan en .gitignore).