Initial commit: DJ Visualizer

Electron app with audio-reactive fractal/abstract visuals on an external
display. Dual-window setup (control panel + fullscreen output), 8 WebGL
scenes, audio from file/live input/system audio, image and scrolling-text
overlays.

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 12:44:17 +02:00
commit b32ea47ae5
14 files changed
+5748

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
// Audio engine: builds a Web Audio graph from either a loaded file or a live
// input device, runs an FFT analyser, and exposes smoothed bass/mid/treble/
// level values plus a simple beat detector.
class AudioEngine {
constructor() {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.ctx.createAnalyser();
this.analyser.fftSize = 2048;
this.analyser.smoothingTimeConstant = 0.78;
this.freq = new Uint8Array(this.analyser.frequencyBinCount);
// Output gain so a loaded file is audible; live input stays muted to
// avoid feedback (it's already playing through the system).
this.outGain = this.ctx.createGain();
this.outGain.gain.value = 1.0;
this.outGain.connect(this.ctx.destination);
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';
// Smoothed band values and beat state.
this.gain = 1.0; // user reactivity multiplier
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;
}
resume() { if (this.ctx.state === 'suspended') this.ctx.resume(); }
_disconnectSource() {
if (this.sourceNode) { try { this.sourceNode.disconnect(); } catch (e) {} this.sourceNode = null; }
if (this.mediaEl) { this.mediaEl.pause(); this.mediaEl.src = ''; this.mediaEl = null; }
if (this.stream) { this.stream.getTracks().forEach(t => t.stop()); this.stream = null; }
}
// Play a File/Blob, or a src URL (e.g. file://…), through the analyser and
// the speakers.
async loadFile(fileOrSrc) {
this.resume();
this._disconnectSource();
const el = new Audio();
el.src = typeof fileOrSrc === 'string' ? fileOrSrc : URL.createObjectURL(fileOrSrc);
el.loop = true;
el.crossOrigin = 'anonymous';
this.mediaEl = el;
const src = this.ctx.createMediaElementSource(el);
src.connect(this.analyser);
src.connect(this.outGain); // file audio is audible
this.sourceNode = src;
this.mode = 'file';
await el.play();
return el;
}
// Use a live input device (mic / line-in / BlackHole). Not routed to output
// to avoid feedback loops.
async useInput(deviceId) {
this.resume();
this._disconnectSource();
const constraints = {
audio: {
deviceId: deviceId ? { exact: deviceId } : undefined,
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false
},
video: false
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
this.stream = stream;
const src = this.ctx.createMediaStreamSource(stream);
src.connect(this.analyser); // analyser only — no output
this.sourceNode = src;
this.mode = 'input';
}
async listInputDevices() {
// Labels are only populated after permission is granted.
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 === 'audioinput');
}
togglePlay() {
if (!this.mediaEl) return null;
if (this.mediaEl.paused) { this.mediaEl.play(); return true; }
this.mediaEl.pause();
return false;
}
// Sample the analyser and update smoothed values. Call once per frame.
update() {
this.analyser.getByteFrequencyData(this.freq);
const bins = this.freq.length; // 1024 bins, ~0..(sr/2)
// Roughly: bass 0-6%, mid 6-25%, treble 25-65% of spectrum.
const bassEnd = Math.floor(bins * 0.06);
const midEnd = Math.floor(bins * 0.25);
const trebEnd = Math.floor(bins * 0.65);
let b = 0, m = 0, t = 0;
for (let i = 0; i < bassEnd; i++) b += this.freq[i];
for (let i = bassEnd; i < midEnd; i++) m += this.freq[i];
for (let i = midEnd; i < trebEnd; i++) t += this.freq[i];
b = b / (bassEnd * 255);
m = m / ((midEnd - bassEnd) * 255);
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);
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.
this._bassAvg = this._bassAvg * 0.94 + b * 0.06;
this._beatCooldown -= 1;
if (b > this._bassAvg * 1.35 && b > 0.12 && this._beatCooldown <= 0) {
this.beat = 1.0;
this._beatCooldown = 8; // ~min frames between beats
} else {
this.beat *= 0.86; // decay
}
return this;
}
_smooth(cur, target, attack, release) {
const k = target > cur ? attack : release;
return cur + (target - cur) * k;
}
get values() {
return { bass: this.bass, mid: this.mid, treble: this.treble, level: this.level, beat: this.beat };
}
}
window.AudioEngine = AudioEngine;