Effects engine, SVG/VU/silhouette categories, pad bank, MP4 recording, dual-window UI

- Parametric effect engine: 23 shader families × palettes × variants (~2300 presets)
  with a filterable library and an effect sequence (auto-cycle by time or beat).
- Effect categories: fractals/abstract, dancing silhouettes, SVG/Image source
  (loads any SVG/image as a recolourable audio-reactive effect; 50+ bundled CC0 SVGs),
  and VU-Meter (32-band spectrum bars, analogue needle, stereo LEDs).
- Dual-window architecture: control panel + fullscreen output on an external display
  (reliable via simple-fullscreen); tabbed control UI (Audio, Effetti, Pad, Immagini,
  Testo, Registra, Schermo).
- Audio: file playback + live input + output device selection, per-band visual EQ,
  audio playlist (reorder, hotkeys, per-track play/pause, repeat).
- Pad bank: 5×4 launchpad with persistent assignments; pressing a pad stops the current.
- Overlays: slideshow images (resizable), two freely positioned/resizable logos,
  scrolling ticker text.
- MP4 recording of the composited output (visual + overlays + audio) via MediaRecorder
  + ffmpeg, with selectable aspect ratios (16:9, 9:16, 1:1, 4:3, 3:4, 21:9).
- App icon (.icns) and dock icon; author Dj LuZa.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Opus 4.8 committed 2026-06-19 15:25:39 +02:00
1 parent b32ea47ae5
commit 47728d0d2a
76 files changed
+21794 -459

No files matched your search

+66 -10
View File
@@ -16,17 +16,37 @@ class AudioEngine {
this.outGain.gain.value = 1.0;
this.outGain.connect(this.ctx.destination);
// Tap for recording: every source also feeds this destination so the
// recorder captures file playback and live input alike.
this.recordDest = this.ctx.createMediaStreamDestination();
// Silent keep-alive so the audio track always produces frames (otherwise,
// with no source connected, the recorder stalls with empty data).
try {
const keep = this.ctx.createConstantSource();
keep.offset.value = 0;
keep.connect(this.recordDest);
keep.start();
} catch (e) { /* ConstantSource unsupported: live/file audio still flows */ }
this.sourceNode = null; // current MediaElement / MediaStream source
this.mediaEl = null; // <audio> element when playing a file
this.stream = null; // MediaStream when using live input
this.mode = 'none';
this.onEnded = null; // callback fired when a (non-looping) track ends
// Smoothed band values and beat state.
this.gain = 1.0; // user reactivity multiplier
this.gain = 1.0; // master reactivity multiplier
this.bassGain = 1.0; // per-band visual intensity
this.midGain = 1.0;
this.trebleGain = 1.0;
this.bass = 0; this.mid = 0; this.treble = 0; this.level = 0;
this.beat = 0;
this._bassAvg = 0; // running average for beat detection
this._beatCooldown = 0;
// Log-spaced spectrum for VU/bar visualisers.
this.NB = 32;
this.spectrum = new Float32Array(this.NB);
}
resume() { if (this.ctx.state === 'suspended') this.ctx.resume(); }
@@ -38,20 +58,22 @@ class AudioEngine {
}
// Play a File/Blob, or a src URL (e.g. file://…), through the analyser and
// the speakers.
async loadFile(fileOrSrc) {
// the speakers. opts.loop controls looping (default true for a single file).
async loadFile(fileOrSrc, opts = {}) {
this.resume();
this._disconnectSource();
const el = new Audio();
el.src = typeof fileOrSrc === 'string' ? fileOrSrc : URL.createObjectURL(fileOrSrc);
el.loop = true;
el.loop = opts.loop !== undefined ? opts.loop : true;
el.crossOrigin = 'anonymous';
el.onended = () => { if (this.onEnded) this.onEnded(); };
this.mediaEl = el;
const src = this.ctx.createMediaElementSource(el);
src.connect(this.analyser);
src.connect(this.outGain); // file audio is audible
src.connect(this.recordDest); // and captured by the recorder
this.sourceNode = src;
this.mode = 'file';
@@ -78,7 +100,8 @@ class AudioEngine {
this.stream = stream;
const src = this.ctx.createMediaStreamSource(stream);
src.connect(this.analyser); // analyser only — no output
src.connect(this.analyser); // analyser only — no output (avoid feedback)
src.connect(this.recordDest); // but capture it when recording
this.sourceNode = src;
this.mode = 'input';
}
@@ -90,6 +113,25 @@ class AudioEngine {
return devices.filter(d => d.kind === 'audioinput');
}
async listOutputDevices() {
try { await navigator.mediaDevices.getUserMedia({ audio: true }).then(s => s.getTracks().forEach(t => t.stop())); } catch (e) {}
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(d => d.kind === 'audiooutput');
}
// Route the file/playback audio to a specific output device (speaker).
// Uses AudioContext.setSinkId (Chromium 110+); empty id = system default.
async setOutputDevice(deviceId) {
if (typeof this.ctx.setSinkId !== 'function') return false;
try {
await this.ctx.setSinkId(deviceId || '');
return true;
} catch (e) {
console.warn('setSinkId fallito:', e);
return false;
}
}
togglePlay() {
if (!this.mediaEl) return null;
if (this.mediaEl.paused) { this.mediaEl.play(); return true; }
@@ -115,10 +157,10 @@ class AudioEngine {
t = t / ((trebEnd - midEnd) * 255);
const g = this.gain;
// Smooth toward new values (attack/release) and apply user gain.
this.bass = this._smooth(this.bass, Math.min(1, b * 1.6 * g), 0.5, 0.12);
this.mid = this._smooth(this.mid, Math.min(1, m * 2.2 * g), 0.5, 0.12);
this.treble = this._smooth(this.treble, Math.min(1, t * 3.0 * g), 0.6, 0.15);
// Smooth toward new values (attack/release), applying master + per-band gain.
this.bass = this._smooth(this.bass, Math.min(1, b * 1.6 * g * this.bassGain), 0.5, 0.12);
this.mid = this._smooth(this.mid, Math.min(1, m * 2.2 * g * this.midGain), 0.5, 0.12);
this.treble = this._smooth(this.treble, Math.min(1, t * 3.0 * g * this.trebleGain), 0.6, 0.15);
this.level = this._smooth(this.level, Math.min(1, (b + m + t) / 3 * 2.0 * g), 0.4, 0.1);
// Beat detection: bass spike above running average.
@@ -131,6 +173,20 @@ class AudioEngine {
this.beat *= 0.86; // decay
}
// Log-spaced spectrum bands (for VU-meter style effects).
const NB = this.NB;
const minBin = 1;
for (let i = 0; i < NB; i++) {
const lo = Math.floor(minBin * Math.pow(bins / minBin, i / NB));
let hi = Math.floor(minBin * Math.pow(bins / minBin, (i + 1) / NB));
if (hi <= lo) hi = lo + 1;
let sum = 0;
for (let k = lo; k < hi; k++) sum += this.freq[k];
let val = sum / ((hi - lo) * 255);
val = Math.min(1, val * (1.4 + i * 0.06) * g); // tilt up the highs + user gain
this.spectrum[i] = this.spectrum[i] * 0.55 + val * 0.45; // smooth
}
return this;
}
@@ -140,7 +196,7 @@ class AudioEngine {
}
get values() {
return { bass: this.bass, mid: this.mid, treble: this.treble, level: this.level, beat: this.beat };
return { bass: this.bass, mid: this.mid, treble: this.treble, level: this.level, beat: this.beat, spectrum: this.spectrum };
}
}