Port the Auto VJ director from the Mac app (verified live on the Pi)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Fable 5 committed 2026-07-17 15:32:21 +02:00
1 parent bf24d78e87
commit 2f6999996d
5 files changed
+99 -1

No files matched your search

+43 -1
View File
@@ -314,7 +314,16 @@ async function reportDevices() {
// ---------------------------------------------------------------- command bus
djv.onControl(async (m) => {
switch (m.type) {
case 'effect': viz.setEffect(m.effect); break;
case 'effect':
// A manual pick takes over from the Auto VJ.
if (autoVj) { autoVj = false; djv.report({ type: 'autoVj', on: false }); }
viz.setEffect(m.effect);
break;
case 'autoVj':
autoVj = !!m.on;
if (autoVj) { avLastSwitch = 0; avBeatMark = audio.beatCount || 0; }
djv.report({ type: 'autoVj', on: autoVj });
break;
case 'svg': loadCustomTexture(m.dataUrl); break;
case 'recStart': startRecording(); break;
case 'recStop': stopRecording(m); break;
@@ -477,10 +486,43 @@ djv.onControl(async (m) => {
const startTime = performance.now();
let frames = 0, fpsT = performance.now(), fps = 0, lastReport = 0, prevBeat = 0, lastProgress = 0;
// ---- Auto VJ: a director that adapts the visuals to the playing track ------
// Uses the engine's musical features (mood/bpm/drop/beatCount): switches preset
// every 16 beats (or ~14s without beats), immediately on a detected drop, and
// draws from pools of families that fit the current mood.
let autoVj = false, avLastSwitch = 0, avBeatMark = 0, avRecent = [];
const AV_POOLS = {
quiet: ['Cielo', 'Aurora', 'Mare', 'Montagne', 'Galassia', 'Nuvole', 'Fluido', 'Onde'],
groove: ['Plasma', 'Vortice', 'Truchet', 'Esagoni', 'Spirale', 'Cellule', 'Gyroide',
'Fluido', 'Fluido Onda', 'Fluido Vortici', 'Griglia Neon', 'Solidi 3D'],
peak: ['Julia', 'Iperspazio', 'Tunnel', 'Moiré', 'Fluido Anello', 'Fluido Fuoco',
'Vortice', 'Griglia Neon', 'Cristalli']
};
function avPick(mood) {
const fams = AV_POOLS[mood] || AV_POOLS.groove;
const cands = window.EFFECTS.list.filter(e => fams.indexOf(e.familyName) >= 0 &&
(mood === 'peak' ? e.speed >= 1.0 : e.speed <= 1.4) && avRecent.indexOf(e.name) < 0);
const e = cands.length ? cands[Math.floor(Math.random() * cands.length)] : window.EFFECTS.list[0];
avRecent = [e.name].concat(avRecent).slice(0, 6); // don't repeat the last few
return e;
}
function avTick(now) {
if (!autoVj) return;
const dropNow = audio.drop === 1;
const due = (audio.beatCount - avBeatMark) >= 16 || (now - avLastSwitch) > 14000;
if (!due && !dropNow) return;
avBeatMark = audio.beatCount; avLastSwitch = now;
const mood = dropNow ? 'peak' : audio.mood;
const e = avPick(mood);
viz.setEffect(e);
djv.report({ type: 'autoVj', on: true, name: e.name, mood, bpm: audio.bpm || 0 });
}
function frame() {
const t = (performance.now() - startTime) / 1000;
audio.update();
const a = audio.values;
avTick(performance.now());
viz.render(t * speed, a);
// Report beat rising-edges so the control window can auto-cycle effects.