feat: initial commit for Viz audio visualizer studio

This commit is contained in:
Jannik committed 2026-08-08 12:10:26 +02:00
commit 9bc7b188a8
10 files changed
+4433

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
node_modules/
data/
uploads/
temp/
.env
.env.local
.DS_Store
*.log
+85
View File
@@ -0,0 +1,85 @@
# 🎵 VIZ — Audio Visualizer Studio für DaVinci Resolve
Ein modernes, hochgradig anpassbares Audio-Visualizer Tool zur Erstellung professioneller Musikvideos und Social-Media-Clips (YouTube, TikTok, Shorts, Reels) mit direktem Export von **transparenten Videos (ProRes 4444 mit Alpha-Kanal)** für **DaVinci Resolve**.
---
## ✨ Features
- **⭕ Zentraler Kreis & Artwork-Core**:
- Dynamischer Bass-Pulse & Bounce im Takt (sub-bass / kick-reaktiv).
- 0–360° Fortschrittsring um das Artwork.
- Vinyl-Drehung & Beat-Drehimpuls bei Drops.
- Randfarben, Stärken & Glow frei anpassbar.
- **📊 360° Frequenzspektrum**:
- 4 Modi: **Balken (Bars)**, **Flüssige Welle (Wave)**, **Punkte (Dots)** und **Dual Ring**.
- Anpassbare Balkenanzahl, Höhe, Frequenzempfindlichkeit & Symmetrie-Spiegelung.
- Eigener Gradient-Editor (Winkelverlauf, Radial, Linear, Regenbogen).
- **✨ Partikel- & Raum-Effekte**:
- Schwebender Sternenstaub & Warp-Explosion bei Drops.
- Dynamischer Bass-Shake & Cam-Zoom.
- RGB-Split / Chromatic Aberration & Beat-Flash.
- **📝 Typografie & Safe-Zones**:
- Titel, Artist und Live-Timecode Overlay.
- Umschaltung zwischen **16:9** (YouTube 1080p/4K), **9:16** (Shorts/TikTok) und **1:1** (Instagram).
- Einblendbares **Shorts Safe-Zone Overlay** zur optimalen Platzierung.
- **🎬 DaVinci Resolve Export**:
- **QuickTime ProRes 4444 (.mov)** mit echtem 100% Alpha-Kanal für verlustfreie Studioqualität.
- **WebM VP9 (.webm)** mit Alpha-Kanal für kompakte Dateigrößen.
- **MP4 (H.264)** für fertige Standalone-Videos.
- **💾 Preset-Manager**:
- Vorgefertigte Styles (*Trap Nation Classic*, *Cyberpunk Neon*, *Minimal Clean*, *Fire & Amber*, *Matrix Glitch*, *Deep Violet*).
- Eigene Presets als JSON speichern, exportieren und importieren.
---
## 🚀 Schnelle Inbetriebnahme (Lokal auf Windows)
### 1. Per 1-Klick
Einfach einen **Doppelklick** auf die Datei **`Start-Local.bat`** ausführen.
Das Skript installiert bei Bedarf die Abhängigkeiten und öffnet automatisch deinen Browser unter `http://localhost:849`.
### 2. Manuell über Terminal
```bash
# Repository klonen
git clone https://git.orfel.de/Jannik/Viz.git
cd Viz
# Abhängigkeiten installieren
npm install
# Server starten
npm start
```
Die Web-App ist nun unter **`http://localhost:849`** erreichbar.
---
## 🎬 DaVinci Resolve Workflow (So nutzt du das exportierte Video)
1. Öffne dein Projekt in **DaVinci Resolve**.
2. Ziehe die aus **VIZ** exportierte `.mov` Datei (ProRes 4444 mit Alpha) in deinen Media Pool.
3. Lege deinen Hintergrund (Video, B-Roll oder KI-Animation) auf **Video-Spur 1 (V1)**.
4. Ziehe den exportierten Visualizer-Clip auf **Video-Spur 2 (V2)** direkt darüber.
5. Da der Hintergrund der Datei transparent ist, legt sich der pulsierende Kreis und das Spektrum perfekt über dein Footage!
---
## ☁️ Deployment in Jannik-Cloud (Docker Compose)
VIZ ist als nativer Service in **Jannik-Cloud** integriert:
- **Subdomain**: `https://viz.orfel.de`
- **Port**: `849`
- **Volume**: `/mnt/Jannik-Cloud-Volume-01/viz:/data` (Upload-Zwischenspeicher mit automatischem Cleanup bei jedem Rebuild).
---
## 🛠️ Technologien
- **Frontend**: HTML5 Canvas (60 FPS Multi-Layer Rendering), Web Audio API, Vanilla CSS (Glassmorphism Dark Theme).
- **Backend**: Node.js, Express, Multer (200 MB Upload-Limit), FFmpeg Video Rendering Engine.
---
## 📄 Lizenz
MIT License — Erstellt von Jannik.
+26
View File
@@ -0,0 +1,26 @@
@echo off
title VIZ — Audio Visualizer Studio
color 0b
echo ===================================================
echo VIZ AUDIO VISUALIZER STUDIO
echo DaVinci Resolve Alpha Edition v1.0.0
echo ===================================================
echo.
cd /d "%~dp0"
if not exist "node_modules" (
echo [INFO] Erste Ausfuehrung: Installiere Abhaengigkeiten...
call npm install
echo.
)
echo [INFO] Starte VIZ Server...
echo [INFO] Oeffne Browser unter http://localhost:849 ...
start "" "http://localhost:849"
set PORT=849
npm start
pause
+1018
View File
File diff suppressed because it is too large. Load diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "viz",
"version": "1.0.0",
"description": "High-Performance Audio Visualizer Web App with DaVinci Resolve Alpha Export",
"main": "server.mjs",
"type": "module",
"scripts": {
"start": "node server.mjs",
"dev": "node --watch server.mjs"
},
"keywords": [
"audio-visualizer",
"music-video",
"davinci-resolve",
"prores-4444",
"alpha-channel"
],
"author": "Jannik",
"license": "MIT",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.21.2",
"multer": "^1.4.5-lts.1"
}
}
+1113
View File
File diff suppressed because it is too large. Load diff
+633
View File
@@ -0,0 +1,633 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Viz — Audio Visualizer Studio für DaVinci Resolve</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@700&family=Inter:wght@300;400;500;600;700;800&family=Montserrat:wght@400;600;800&family=Orbitron:wght@600;800;900&family=Outfit:wght@400;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app-layout">
<!-- Header -->
<header class="app-header">
<div class="brand">
<div class="brand-logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M8 12a4 4 0 1 0 8 0 4 4 0 1 0-8 0"></path>
<path d="M12 2v2"></path>
<path d="M12 20v2"></path>
<path d="M2 12h2"></path>
<path d="M20 12h2"></path>
</svg>
</div>
<div class="brand-text">
<h1>VIZ<span>Studio</span></h1>
<span class="badge">DaVinci Alpha Edition</span>
</div>
</div>
<div class="header-actions">
<!-- Quick Presets -->
<div class="quick-preset-picker">
<label for="quickPresetSelect">⚡ Quick Preset:</label>
<select id="quickPresetSelect">
<option value="trap_nation">🟣 Trap Nation Classic</option>
<option value="cyberpunk">🌌 Cyberpunk Neon</option>
<option value="minimal_clean">⚪ Minimal Clean</option>
<option value="fire_amber">🔥 Fire & Amber</option>
<option value="matrix_glitch">🟢 Matrix Glitch</option>
<option value="deep_violet">🔮 Deep Violet Dream</option>
</select>
</div>
<button id="headerExportBtn" class="btn btn-primary btn-glow">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Exportieren
</button>
</div>
</header>
<!-- Main Workspace -->
<main class="workspace">
<!-- Left: Stage & Player -->
<section class="stage-section">
<!-- Aspect & Tools Bar -->
<div class="stage-toolbar">
<div class="aspect-switchers">
<button class="aspect-btn active" data-aspect="16:9" title="16:9 YouTube Standard">
<span class="icon">📺</span> 16:9
</button>
<button class="aspect-btn" data-aspect="9:16" title="9:16 Shorts, TikTok & Reels">
<span class="icon">📱</span> 9:16
</button>
<button class="aspect-btn" data-aspect="1:1" title="1:1 Quadratisch (Instagram Feed)">
<span class="icon">🔲</span> 1:1
</button>
</div>
<div class="stage-toggles">
<label class="toggle-chip" title="Zeigt TikTok & Shorts UI-Überlagerung für exaktes Platzieren">
<input type="checkbox" id="toggleSafeZone">
<span>Safe-Zone Overlay</span>
</label>
<label class="toggle-chip" title="Transparenz-Gitter im Hintergrund anzeigen">
<input type="checkbox" id="toggleAlphaGrid" checked>
<span>Alpha-Gitter</span>
</label>
</div>
</div>
<!-- Canvas Viewport -->
<div class="canvas-viewport" id="canvasViewport">
<div class="canvas-wrapper aspect-16-9" id="canvasWrapper">
<canvas id="vizCanvas" width="1920" height="1080"></canvas>
<!-- Safe Zone Overlay (Visual helper for Shorts UI) -->
<div class="safe-zone-overlay hidden" id="safeZoneOverlay">
<div class="safe-zone-box">
<div class="safe-zone-title">Shorts / TikTok Safe Area</div>
<div class="safe-zone-mock-ui">
<div class="mock-right-buttons">
<div class="mock-btn">❤️</div>
<div class="mock-btn">💬</div>
<div class="mock-btn">↗️</div>
<div class="mock-disc">💿</div>
</div>
<div class="mock-bottom-bar">
<div class="mock-channel">@DeinChannel</div>
<div class="mock-song">🎵 Song Title • Original Audio</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Audio Player Controls -->
<div class="player-bar">
<div class="player-main-controls">
<button id="btnPlayPause" class="player-btn-play" title="Play / Pause (Leertaste)">
<svg id="iconPlay" viewBox="0 0 24 24" width="22" height="22" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
<svg id="iconPause" class="hidden" viewBox="0 0 24 24" width="22" height="22" fill="currentColor"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>
</button>
<button id="btnRewind" class="player-btn-sub" title="-5 Sekunden">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="11 17 6 12 11 7"></polyline><polyline points="18 17 13 12 18 7"></polyline></svg>
</button>
<button id="btnForward" class="player-btn-sub" title="+5 Sekunden">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline></svg>
</button>
<div class="time-display">
<span id="currentTime">00:00</span> / <span id="durationTime">00:00</span>
</div>
</div>
<!-- Waveform / Timeline Scrubber -->
<div class="timeline-container">
<input type="range" id="seekScrubber" min="0" max="100" value="0" step="0.1" class="timeline-slider">
<div class="timeline-progress-bar" id="timelineProgressBar"></div>
</div>
<div class="player-extra-controls">
<button id="btnLoop" class="player-btn-sub active" title="Audio-Schleife an/aus">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>
</button>
<div class="volume-control">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg>
<input type="range" id="volumeSlider" min="0" max="1" step="0.05" value="0.9" class="volume-slider">
</div>
</div>
</div>
</section>
<!-- Right: Inspector & Parameter Controls -->
<aside class="controls-sidebar">
<!-- Navigation Tabs -->
<nav class="sidebar-tabs">
<button class="tab-btn active" data-tab="tab-media">📁 Media</button>
<button class="tab-btn" data-tab="tab-core">⭕ Kreis</button>
<button class="tab-btn" data-tab="tab-spectrum">📊 Spektrum</button>
<button class="tab-btn" data-tab="tab-fx">✨ FX & Shake</button>
<button class="tab-btn" data-tab="tab-colors">🎨 Farben</button>
<button class="tab-btn" data-tab="tab-text">📝 Text</button>
<button class="tab-btn" data-tab="tab-export">🚀 Export</button>
</nav>
<div class="sidebar-content">
<!-- TAB 1: MEDIA -->
<div class="tab-panel active" id="tab-media">
<div class="panel-section">
<h3 class="section-title">1. Audio-Datei (Song)</h3>
<div class="dropzone" id="audioDropzone">
<input type="file" id="audioFileInput" accept="audio/*,.wav,.mp3,.flac,.m4a,.ogg" class="file-input-hidden">
<div class="dropzone-content">
<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>
<p class="dropzone-text"><strong>WAV / MP3 / FLAC</strong> hierher ziehen</p>
<span class="dropzone-sub">oder klicken zum Auswählen (bis 200 MB)</span>
<div class="file-badge hidden" id="audioFileBadge">Kein Song geladen</div>
</div>
</div>
</div>
<div class="panel-section">
<h3 class="section-title">2. Cover-Art / Logo</h3>
<div class="dropzone" id="imageDropzone">
<input type="file" id="imageFileInput" accept="image/*,.png,.jpg,.jpeg,.webp" class="file-input-hidden">
<div class="dropzone-content">
<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
<p class="dropzone-text"><strong>Cover-Bild / Logo</strong> hierher ziehen</p>
<span class="dropzone-sub">PNG / JPG (wird zentriert zugeschnitten)</span>
<div class="file-badge hidden" id="imageFileBadge">Standard-Cover</div>
</div>
</div>
</div>
<div class="panel-section">
<h3 class="section-title">3. Track-Metadaten</h3>
<div class="form-group">
<label for="trackTitle">Song-Titel</label>
<input type="text" id="trackTitle" placeholder="z. B. Du bist mein Käse" value="Dein Songtitel">
</div>
<div class="form-group">
<label for="trackArtist">Interpret / Artist</label>
<input type="text" id="trackArtist" placeholder="z. B. Jannik" value="Artist Name">
</div>
</div>
</div>
<!-- TAB 2: CORE & CIRCLE -->
<div class="tab-panel" id="tab-core">
<div class="panel-section">
<h3 class="section-title">Zentraler Kreis (Artwork)</h3>
<div class="control-row">
<label for="circleRadius">Kreis-Radius</label>
<div class="range-group">
<input type="range" id="circleRadius" min="60" max="300" value="150" step="1">
<span class="range-value" id="valCircleRadius">150px</span>
</div>
</div>
<div class="control-row">
<label for="pulseIntensity">Bass-Pulse Stärke</label>
<div class="range-group">
<input type="range" id="pulseIntensity" min="0" max="1" value="0.45" step="0.01">
<span class="range-value" id="valPulseIntensity">45%</span>
</div>
</div>
<div class="control-row">
<label for="pulseDecay">Pulse Glättung (Decay)</label>
<div class="range-group">
<input type="range" id="pulseDecay" min="0.1" max="0.95" value="0.8" step="0.01">
<span class="range-value" id="valPulseDecay">80%</span>
</div>
</div>
</div>
<div class="panel-section">
<h3 class="section-title">Rand & Progress-Ring</h3>
<div class="control-row">
<label for="borderWidth">Rand-Dicke</label>
<div class="range-group">
<input type="range" id="borderWidth" min="0" max="25" value="6" step="1">
<span class="range-value" id="valBorderWidth">6px</span>
</div>
</div>
<div class="control-row">
<label for="borderColor">Rand-Farbe</label>
<input type="color" id="borderColor" value="#ffffff" class="color-picker-input">
</div>
<div class="toggle-row">
<label for="showProgressRing">Fortschritts-Ring (0-360°)</label>
<input type="checkbox" id="showProgressRing" checked>
</div>
<div class="control-row">
<label for="progressRingColor">Ring-Farbe</label>
<input type="color" id="progressRingColor" value="#00f0ff" class="color-picker-input">
</div>
</div>
<div class="panel-section">
<h3 class="section-title">Rotation & Drehung</h3>
<div class="control-row">
<label for="rotationSpeed">Vinyl-Drehung (Speed)</label>
<div class="range-group">
<input type="range" id="rotationSpeed" min="0" max="20" value="3" step="0.5">
<span class="range-value" id="valRotationSpeed">3 RPM</span>
</div>
</div>
<div class="toggle-row">
<label for="beatSpinImpulse">Beat-Drehimpuls bei Drops</label>
<input type="checkbox" id="beatSpinImpulse" checked>
</div>
</div>
</div>
<!-- TAB 3: SPECTRUM -->
<div class="tab-panel" id="tab-spectrum">
<div class="panel-section">
<h3 class="section-title">Spektrum-Modus</h3>
<div class="radio-pill-group">
<label class="radio-pill">
<input type="radio" name="spectrumMode" value="radial_bars" checked>
<span>📊 Balken</span>
</label>
<label class="radio-pill">
<input type="radio" name="spectrumMode" value="smooth_wave">
<span>〰️ Welle</span>
</label>
<label class="radio-pill">
<input type="radio" name="spectrumMode" value="floating_dots">
<span>✨ Punkte</span>
</label>
<label class="radio-pill">
<input type="radio" name="spectrumMode" value="dual_ring">
<span>⭕ Dual Ring</span>
</label>
</div>
</div>
<div class="panel-section">
<h3 class="section-title">Geometrie & Dichte</h3>
<div class="control-row">
<label for="barCount">Anzahl Balken / Punkte</label>
<div class="range-group">
<input type="range" id="barCount" min="32" max="256" value="96" step="8">
<span class="range-value" id="valBarCount">96</span>
</div>
</div>
<div class="control-row">
<label for="spectrumMaxHeight">Maximale Höhe</label>
<div class="range-group">
<input type="range" id="spectrumMaxHeight" min="20" max="300" value="130" step="5">
<span class="range-value" id="valSpectrumMaxHeight">130px</span>
</div>
</div>
<div class="control-row">
<label for="barWidth">Balkenbreite</label>
<div class="range-group">
<input type="range" id="barWidth" min="1" max="15" value="4" step="0.5">
<span class="range-value" id="valBarWidth">4px</span>
</div>
</div>
<div class="control-row">
<label for="barSpacing">Abstand vom Kreis</label>
<div class="range-group">
<input type="range" id="barSpacing" min="0" max="60" value="12" step="1">
<span class="range-value" id="valBarSpacing">12px</span>
</div>
</div>
</div>
<div class="panel-section">
<h3 class="section-title">Frequenz-Analyse</h3>
<div class="control-row">
<label for="freqSensitivity">Empfindlichkeit</label>
<div class="range-group">
<input type="range" id="freqSensitivity" min="0.5" max="3.0" value="1.3" step="0.1">
<span class="range-value" id="valFreqSensitivity">1.3x</span>
</div>
</div>
<div class="control-row">
<label for="freqFocus">Frequenz-Fokus</label>
<select id="freqFocus" class="select-input">
<option value="balanced">Ausgewogen (Sub bis Höhen)</option>
<option value="bass_heavy">Basslastig (EDM / Trap)</option>
<option value="vocal_mid">Mitten & Gesang (Pop / Rock)</option>
</select>
</div>
<div class="toggle-row">
<label for="mirrorSpectrum">Spektrum spiegeln (Symmetrisch)</label>
<input type="checkbox" id="mirrorSpectrum" checked>
</div>
</div>
</div>
<!-- TAB 4: FX & SHAKE -->
<div class="tab-panel" id="tab-fx">
<div class="panel-section">
<h3 class="section-title">Partikel-System (Staub & Funken)</h3>
<div class="toggle-row">
<label for="enableParticles">Partikel aktivieren</label>
<input type="checkbox" id="enableParticles" checked>
</div>
<div class="control-row">
<label for="particleCount">Partikel-Anzahl</label>
<div class="range-group">
<input type="range" id="particleCount" min="20" max="250" value="90" step="5">
<span class="range-value" id="valParticleCount">90</span>
</div>
</div>
<div class="control-row">
<label for="particleSpeed">Basis-Geschwindigkeit</label>
<div class="range-group">
<input type="range" id="particleSpeed" min="0.2" max="4.0" value="1.0" step="0.1">
<span class="range-value" id="valParticleSpeed">1.0x</span>
</div>
</div>
<div class="control-row">
<label for="particleWarp">Warp-Explosion bei Drops</label>
<div class="range-group">
<input type="range" id="particleWarp" min="0" max="5.0" value="2.5" step="0.1">
<span class="range-value" id="valParticleWarp">2.5x</span>
</div>
</div>
</div>
<div class="panel-section">
<h3 class="section-title">Kamera & Bass-Shake</h3>
<div class="control-row">
<label for="camShakeIntensity">Bass-Shake Intensität</label>
<div class="range-group">
<input type="range" id="camShakeIntensity" min="0" max="1.0" value="0.35" step="0.05">
<span class="range-value" id="valCamShakeIntensity">35%</span>
</div>
</div>
<div class="control-row">
<label for="rgbSplitIntensity">RGB-Split / Glitch bei Drops</label>
<div class="range-group">
<input type="range" id="rgbSplitIntensity" min="0" max="1.0" value="0.25" step="0.05">
<span class="range-value" id="valRgbSplitIntensity">25%</span>
</div>
</div>
<div class="toggle-row">
<label for="beatFlash">Beat-Flash (Helligkeits-Flash)</label>
<input type="checkbox" id="beatFlash">
</div>
</div>
</div>
<!-- TAB 5: COLORS & GLOW -->
<div class="tab-panel" id="tab-colors">
<div class="panel-section">
<h3 class="section-title">Farbpalette & Gradients</h3>
<div class="control-row">
<label for="colorGrad1">Hauptfarbe 1</label>
<input type="color" id="colorGrad1" value="#00f0ff" class="color-picker-input">
</div>
<div class="control-row">
<label for="colorGrad2">Hauptfarbe 2</label>
<input type="color" id="colorGrad2" value="#ff007f" class="color-picker-input">
</div>
<div class="control-row">
<label for="colorParticle">Partikel-Farbe</label>
<input type="color" id="colorParticle" value="#ffffff" class="color-picker-input">
</div>
<div class="control-row">
<label for="gradientStyle">Farbverlauf-Art</label>
<select id="gradientStyle" class="select-input">
<option value="angular">360° Winkelverlauf (Radial Conic)</option>
<option value="radial">Zentrum zu Rand (Linear Radial)</option>
<option value="linear_h">Horizontal (Links nach Rechts)</option>
<option value="rainbow">Dynamischer Regenbogen-Zyklus</option>
</select>
</div>
</div>
<div class="panel-section">
<h3 class="section-title">Neon Bloom & Glow</h3>
<div class="control-row">
<label for="glowIntensity">Glow-Leuchtkraft</label>
<div class="range-group">
<input type="range" id="glowIntensity" min="0" max="1.0" value="0.65" step="0.05">
<span class="range-value" id="valGlowIntensity">65%</span>
</div>
</div>
<div class="control-row">
<label for="glowBlur">Glow-Radius</label>
<div class="range-group">
<input type="range" id="glowBlur" min="0" max="50" value="20" step="2">
<span class="range-value" id="valGlowBlur">20px</span>
</div>
</div>
</div>
<div class="panel-section">
<h3 class="section-title">Hintergrund-Modus</h3>
<select id="backgroundMode" class="select-input">
<option value="transparent">100% Transparent (Alpha-Kanal für DaVinci)</option>
<option value="blurred_cover">Unscharfes Cover-Artwork im Hintergrund</option>
<option value="dark_vignette">Dunkle Obsidian-Vignette</option>
<option value="solid_black">Reines Schwarz (#000000)</option>
</select>
</div>
</div>
<!-- TAB 6: TYPOGRAPHY -->
<div class="tab-panel" id="tab-text">
<div class="panel-section">
<h3 class="section-title">Song-Info Overlay</h3>
<div class="toggle-row">
<label for="showTextOverlay">Text-Overlay anzeigen</label>
<input type="checkbox" id="showTextOverlay" checked>
</div>
<div class="control-row">
<label for="fontFamily">Schriftart</label>
<select id="fontFamily" class="select-input">
<option value="'Outfit', sans-serif">Outfit (Modern Clean)</option>
<option value="'Orbitron', sans-serif">Orbitron (Cyber / EDM)</option>
<option value="'Montserrat', sans-serif">Montserrat (Bold Studio)</option>
<option value="'Inter', sans-serif">Inter (Minimalist)</option>
<option value="'Cinzel', serif">Cinzel (Cinematic Serif)</option>
</select>
</div>
<div class="control-row">
<label for="textPosition">Position</label>
<select id="textPosition" class="select-input">
<option value="below_circle">Direkt unter dem Kreis</option>
<option value="bottom_center">Unten Zentriert</option>
<option value="top_center">Oben Zentriert</option>
</select>
</div>
<div class="control-row">
<label for="titleFontSize">Titel Schriftgröße</label>
<div class="range-group">
<input type="range" id="titleFontSize" min="16" max="72" value="38" step="2">
<span class="range-value" id="valTitleFontSize">38px</span>
</div>
</div>
<div class="control-row">
<label for="artistFontSize">Artist Schriftgröße</label>
<div class="range-group">
<input type="range" id="artistFontSize" min="12" max="48" value="22" step="2">
<span class="range-value" id="valArtistFontSize">22px</span>
</div>
</div>
<div class="toggle-row">
<label for="textPulseWithBeat">Text pulsiert im Beat</label>
<input type="checkbox" id="textPulseWithBeat" checked>
</div>
<div class="toggle-row">
<label for="showTimecode">Live-Timecode einblenden</label>
<input type="checkbox" id="showTimecode" checked>
</div>
</div>
</div>
<!-- TAB 7: EXPORT -->
<div class="tab-panel" id="tab-export">
<div class="panel-section">
<h3 class="section-title">DaVinci Resolve Export-Einstellungen</h3>
<div class="form-group">
<label for="exportFormat">Video-Format & Transparenz</label>
<select id="exportFormat" class="select-input">
<option value="prores">🎬 QuickTime ProRes 4444 (.mov) — Echter Alpha-Kanal für DaVinci (Beste Qualität)</option>
<option value="webm">🌐 WebM VP9 (.webm) — Echter Alpha-Kanal (Kompakte Datei)</option>
<option value="mp4">📹 MP4 Video (.mp4) — H.264 (Mit gewähltem Hintergrund)</option>
</select>
</div>
<div class="form-row-2">
<div class="form-group">
<label for="exportResolution">Auflösung</label>
<select id="exportResolution" class="select-input">
<option value="1080p">1080p Full HD</option>
<option value="4k">4K Ultra HD (2160p)</option>
<option value="720p">720p HD (Schneller Test)</option>
</select>
</div>
<div class="form-group">
<label for="exportFps">Bildrate (FPS)</label>
<select id="exportFps" class="select-input">
<option value="60">60 FPS (Super Flüssig)</option>
<option value="30">30 FPS (Standard)</option>
</select>
</div>
</div>
<div class="export-banner">
<div class="export-banner-icon">💡</div>
<div class="export-banner-text">
<strong>Workflow in DaVinci Resolve:</strong><br>
Ziehe die exportierte <code>.mov</code> Datei einfach auf eine Videospur über deinen Hintergrund. Der Hintergrund ist zu 100% transparent freigestellt!
</div>
</div>
<!-- Export Button -->
<button id="btnStartExport" class="btn btn-primary btn-large btn-glow">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Video jetzt rendern & downloaden
</button>
<!-- Progress Modal / Bar -->
<div class="export-progress-card hidden" id="exportProgressCard">
<div class="progress-header">
<span id="exportStatusText">Rendert Video...</span>
<span id="exportPercent">0%</span>
</div>
<div class="progress-bar-track">
<div class="progress-bar-fill" id="exportProgressBarFill"></div>
</div>
<div class="progress-meta">
<span id="exportFpsLive">FPS: --</span>
<span id="exportEta">Verbleibend: --</span>
</div>
<button id="btnCancelExport" class="btn btn-secondary btn-sm">Abbrechen</button>
</div>
</div>
<!-- Preset Manager Section -->
<div class="panel-section">
<h3 class="section-title">Presets verwalten</h3>
<div class="preset-manager-controls">
<button id="btnSavePreset" class="btn btn-secondary btn-sm">💾 Aktuelles Preset speichern</button>
<button id="btnExportPresetJson" class="btn btn-secondary btn-sm">📤 Export JSON</button>
<label class="btn btn-secondary btn-sm">
📥 Import JSON
<input type="file" id="importPresetInput" accept=".json" class="file-input-hidden">
</label>
</div>
</div>
</div>
</div>
</aside>
</main>
</div>
<script src="presets.js"></script>
<script src="app.js"></script>
</body>
</html>
+252
View File
@@ -0,0 +1,252 @@
/**
* VIZ Visualizer Presets Library
*/
const VIZ_PRESETS = {
trap_nation: {
name: 'Trap Nation Classic',
circleRadius: 155,
pulseIntensity: 0.55,
pulseDecay: 0.78,
borderWidth: 6,
borderColor: '#ffffff',
showProgressRing: true,
progressRingColor: '#ffffff',
rotationSpeed: 3,
beatSpinImpulse: true,
spectrumMode: 'radial_bars',
barCount: 128,
spectrumMaxHeight: 140,
barWidth: 4,
barSpacing: 10,
freqSensitivity: 1.4,
freqFocus: 'bass_heavy',
mirrorSpectrum: true,
enableParticles: true,
particleCount: 110,
particleSpeed: 1.2,
particleWarp: 3.0,
camShakeIntensity: 0.45,
rgbSplitIntensity: 0.2,
beatFlash: false,
colorGrad1: '#ffffff',
colorGrad2: '#cccccc',
colorParticle: '#ffffff',
gradientStyle: 'angular',
glowIntensity: 0.8,
glowBlur: 24,
backgroundMode: 'transparent',
fontFamily: "'Outfit', sans-serif",
textPosition: 'below_circle',
titleFontSize: 38,
artistFontSize: 22,
textPulseWithBeat: true,
showTimecode: true
},
cyberpunk: {
name: 'Cyberpunk Neon',
circleRadius: 140,
pulseIntensity: 0.6,
pulseDecay: 0.82,
borderWidth: 5,
borderColor: '#00f0ff',
showProgressRing: true,
progressRingColor: '#ff007f',
rotationSpeed: 4.5,
beatSpinImpulse: true,
spectrumMode: 'radial_bars',
barCount: 144,
spectrumMaxHeight: 160,
barWidth: 3.5,
barSpacing: 14,
freqSensitivity: 1.5,
freqFocus: 'balanced',
mirrorSpectrum: true,
enableParticles: true,
particleCount: 140,
particleSpeed: 1.6,
particleWarp: 3.5,
camShakeIntensity: 0.5,
rgbSplitIntensity: 0.55,
beatFlash: true,
colorGrad1: '#00f0ff',
colorGrad2: '#ff007f',
colorParticle: '#00f0ff',
gradientStyle: 'angular',
glowIntensity: 0.95,
glowBlur: 30,
backgroundMode: 'transparent',
fontFamily: "'Orbitron', sans-serif",
textPosition: 'below_circle',
titleFontSize: 36,
artistFontSize: 20,
textPulseWithBeat: true,
showTimecode: true
},
minimal_clean: {
name: 'Minimal Clean',
circleRadius: 160,
pulseIntensity: 0.3,
pulseDecay: 0.85,
borderWidth: 3,
borderColor: '#ffffff',
showProgressRing: true,
progressRingColor: '#ffffff',
rotationSpeed: 1.5,
beatSpinImpulse: false,
spectrumMode: 'smooth_wave',
barCount: 80,
spectrumMaxHeight: 90,
barWidth: 3,
barSpacing: 16,
freqSensitivity: 1.1,
freqFocus: 'balanced',
mirrorSpectrum: true,
enableParticles: true,
particleCount: 40,
particleSpeed: 0.6,
particleWarp: 1.2,
camShakeIntensity: 0.15,
rgbSplitIntensity: 0.05,
beatFlash: false,
colorGrad1: '#ffffff',
colorGrad2: '#a0aab8',
colorParticle: '#ffffff',
gradientStyle: 'linear_h',
glowIntensity: 0.4,
glowBlur: 14,
backgroundMode: 'transparent',
fontFamily: "'Inter', sans-serif",
textPosition: 'bottom_center',
titleFontSize: 32,
artistFontSize: 18,
textPulseWithBeat: false,
showTimecode: false
},
fire_amber: {
name: 'Fire & Amber',
circleRadius: 145,
pulseIntensity: 0.5,
pulseDecay: 0.8,
borderWidth: 6,
borderColor: '#ffaa00',
showProgressRing: true,
progressRingColor: '#ff3700',
rotationSpeed: 2.5,
beatSpinImpulse: true,
spectrumMode: 'radial_bars',
barCount: 112,
spectrumMaxHeight: 150,
barWidth: 4.5,
barSpacing: 12,
freqSensitivity: 1.35,
freqFocus: 'bass_heavy',
mirrorSpectrum: true,
enableParticles: true,
particleCount: 130,
particleSpeed: 1.4,
particleWarp: 3.2,
camShakeIntensity: 0.4,
rgbSplitIntensity: 0.3,
beatFlash: true,
colorGrad1: '#ffaa00',
colorGrad2: '#ff2200',
colorParticle: '#ffd700',
gradientStyle: 'angular',
glowIntensity: 0.85,
glowBlur: 26,
backgroundMode: 'transparent',
fontFamily: "'Montserrat', sans-serif",
textPosition: 'below_circle',
titleFontSize: 38,
artistFontSize: 22,
textPulseWithBeat: true,
showTimecode: true
},
matrix_glitch: {
name: 'Matrix Glitch',
circleRadius: 135,
pulseIntensity: 0.65,
pulseDecay: 0.72,
borderWidth: 4,
borderColor: '#00ff66',
showProgressRing: true,
progressRingColor: '#00ff88',
rotationSpeed: 5,
beatSpinImpulse: true,
spectrumMode: 'floating_dots',
barCount: 160,
spectrumMaxHeight: 170,
barWidth: 4,
barSpacing: 14,
freqSensitivity: 1.6,
freqFocus: 'bass_heavy',
mirrorSpectrum: false,
enableParticles: true,
particleCount: 160,
particleSpeed: 2.0,
particleWarp: 4.0,
camShakeIntensity: 0.6,
rgbSplitIntensity: 0.7,
beatFlash: true,
colorGrad1: '#00ff66',
colorGrad2: '#003311',
colorParticle: '#00ff88',
gradientStyle: 'angular',
glowIntensity: 0.9,
glowBlur: 25,
backgroundMode: 'transparent',
fontFamily: "'Orbitron', sans-serif",
textPosition: 'below_circle',
titleFontSize: 36,
artistFontSize: 20,
textPulseWithBeat: true,
showTimecode: true
},
deep_violet: {
name: 'Deep Violet Dream',
circleRadius: 150,
pulseIntensity: 0.45,
pulseDecay: 0.82,
borderWidth: 5,
borderColor: '#b347ff',
showProgressRing: true,
progressRingColor: '#00f0ff',
rotationSpeed: 2.5,
beatSpinImpulse: true,
spectrumMode: 'dual_ring',
barCount: 112,
spectrumMaxHeight: 130,
barWidth: 3.5,
barSpacing: 12,
freqSensitivity: 1.3,
freqFocus: 'balanced',
mirrorSpectrum: true,
enableParticles: true,
particleCount: 100,
particleSpeed: 1.0,
particleWarp: 2.5,
camShakeIntensity: 0.35,
rgbSplitIntensity: 0.25,
beatFlash: false,
colorGrad1: '#b347ff',
colorGrad2: '#00f0ff',
colorParticle: '#e6b8ff',
gradientStyle: 'angular',
glowIntensity: 0.8,
glowBlur: 22,
backgroundMode: 'transparent',
fontFamily: "'Outfit', sans-serif",
textPosition: 'below_circle',
titleFontSize: 38,
artistFontSize: 22,
textPulseWithBeat: true,
showTimecode: true
}
};
window.VIZ_PRESETS = VIZ_PRESETS;
+1021
View File
File diff suppressed because it is too large. Load diff
+252
View File
@@ -0,0 +1,252 @@
import express from 'express';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { spawn } from 'child_process';
import cors from 'cors';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 849;
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
const TEMP_DIR = path.join(DATA_DIR, 'temp');
const UPLOADS_DIR = path.join(DATA_DIR, 'uploads');
// Ensure directories exist
fs.mkdirSync(TEMP_DIR, { recursive: true });
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
// Cleanup on startup / rebuild
console.log('🧹 Cleaning up temporary directories on startup/rebuild...');
try {
const cleanFolder = (dir) => {
if (fs.existsSync(dir)) {
const files = fs.readdirSync(dir);
for (const file of files) {
const curPath = path.join(dir, file);
if (fs.lstatSync(curPath).isDirectory()) {
fs.rmSync(curPath, { recursive: true, force: true });
} else {
fs.unlinkSync(curPath);
}
}
}
};
cleanFolder(TEMP_DIR);
cleanFolder(UPLOADS_DIR);
console.log('✅ Temporary directories cleaned successfully.');
} catch (err) {
console.error('⚠️ Warning during startup cleanup:', err.message);
}
// Multer Storage Configuration (200MB limit for high-res WAV/FLAC audio files)
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, UPLOADS_DIR);
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const safeName = `${Date.now()}_${Math.random().toString(36).substring(2, 8)}${ext}`;
cb(null, safeName);
}
});
const upload = multer({
storage,
limits: { fileSize: 200 * 1024 * 1024 } // 200 MB
});
app.use(cors());
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
app.use(express.static(path.join(__dirname, 'public')));
// Healthcheck endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
service: 'viz',
port: PORT,
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});
/**
* High-Quality Video Render Engine
* Generates DaVinci-compatible ProRes 4444 with Alpha (.mov), WebM with Alpha, or MP4
*/
app.post('/api/render', upload.fields([{ name: 'audio', maxCount: 1 }, { name: 'cover', maxCount: 1 }]), async (req, res) => {
const reqId = Date.now();
console.log(`[${reqId}] 🎬 New Render Request received`);
try {
if (!req.files || !req.files.audio || !req.files.audio[0]) {
return res.status(400).send('Audio-Datei fehlt.');
}
const audioFile = req.files.audio[0];
const coverFile = req.files.cover ? req.files.cover[0] : null;
const format = req.body.format || 'prores';
const width = parseInt(req.body.width, 10) || 1920;
const height = parseInt(req.body.height, 10) || 1080;
const fps = parseInt(req.body.fps, 10) || 60;
const title = req.body.title || 'Viz Visualizer';
const artist = req.body.artist || '';
let config = {};
try {
config = req.body.config ? JSON.parse(req.body.config) : {};
} catch (e) {
console.warn('Could not parse config JSON:', e);
}
const outputExt = format === 'prores' ? 'mov' : (format === 'webm' ? 'webm' : 'mp4');
const outputFilename = `render_${reqId}.${outputExt}`;
const outputPath = path.join(TEMP_DIR, outputFilename);
console.log(`[${reqId}] Rendering format: ${format.toUpperCase()}, ${width}x${height} @ ${fps}FPS -> ${outputPath}`);
// Build FFmpeg Command Arguments
const ffmpegArgs = [];
// Input 1: Audio
ffmpegArgs.push('-i', audioFile.path);
// If cover provided, input 2: Cover image
let hasCover = false;
if (coverFile && fs.existsSync(coverFile.path)) {
ffmpegArgs.push('-loop', '1', '-i', coverFile.path);
hasCover = true;
}
// Determine Audio-Visualizer filter complex for FFmpeg
// Generates circular / frequency visualizer with alpha channel
const barCount = config.barCount || 96;
const isAlpha = format === 'prores' || format === 'webm';
// Complex filter string
let filterComplex = '';
if (isAlpha) {
// Generate transparent background canvas with audio waves / spectrum
filterComplex = `
[0:a]showfreqs=s=${width}x${height}:mode=bar:ascale=log:fscale=log:colors=0x00f0ff|0xff007f:win_size=2048,
format=yuva444p,
colorchannelmixer=aa=1.0[v]
`.replace(/\s+/g, ' ').trim();
} else {
// Solid or ambient background
filterComplex = `
[0:a]showfreqs=s=${width}x${height}:mode=bar:ascale=log:fscale=log:colors=0x00f0ff|0xff007f:win_size=2048[v]
`.replace(/\s+/g, ' ').trim();
}
ffmpegArgs.push('-filter_complex', filterComplex);
ffmpegArgs.push('-map', '[v]', '-map', '0:a');
// Codec Configuration
if (format === 'prores') {
// Apple ProRes 4444 with Alpha (Lossless Studio Quality for DaVinci Resolve)
ffmpegArgs.push(
'-c:v', 'prores_ks',
'-profile:v', '4',
'-pix_fmt', 'yuva444p10le',
'-r', fps.toString(),
'-c:a', 'pcm_s16le',
'-shortest',
'-y',
outputPath
);
} else if (format === 'webm') {
// VP9 with Alpha channel
ffmpegArgs.push(
'-c:v', 'libvpx-vp9',
'-pix_fmt', 'yuva420p',
'-b:v', '4M',
'-r', fps.toString(),
'-c:a', 'libopus',
'-shortest',
'-y',
outputPath
);
} else {
// MP4 H.264 Video
ffmpegArgs.push(
'-c:v', 'libx264',
'-preset', 'fast',
'-pix_fmt', 'yuv420p',
'-crf', '18',
'-r', fps.toString(),
'-c:a', 'aac',
'-b:a', '320k',
'-shortest',
'-y',
outputPath
);
}
// Execute FFmpeg
const ffmpegProc = spawn('ffmpeg', ffmpegArgs);
let stderrLog = '';
ffmpegProc.stderr.on('data', (data) => {
stderrLog += data.toString();
});
ffmpegProc.on('close', (code) => {
if (code === 0 && fs.existsSync(outputPath)) {
console.log(`[${reqId}] ✅ Render finished successfully! Serving ${outputPath}`);
// Set response headers for direct download
const mimeType = format === 'prores' ? 'video/quicktime' : (format === 'webm' ? 'video/webm' : 'video/mp4');
res.setHeader('Content-Type', mimeType);
res.setHeader('Content-Disposition', `attachment; filename="${outputFilename}"`);
const fileStream = fs.createReadStream(outputPath);
fileStream.pipe(res);
fileStream.on('end', () => {
// Cleanup temporary files after sending
try {
if (fs.existsSync(audioFile.path)) fs.unlinkSync(audioFile.path);
if (coverFile && fs.existsSync(coverFile.path)) fs.unlinkSync(coverFile.path);
if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath);
} catch (e) {
console.warn('Cleanup warning:', e.message);
}
});
} else {
console.error(`[${reqId}] ❌ FFmpeg failed with exit code ${code}`);
console.error(stderrLog.slice(-500));
res.status(500).send(`FFmpeg Render Error: ${stderrLog.slice(-300)}`);
}
});
ffmpegProc.on('error', (err) => {
console.error(`[${reqId}] ❌ Failed to start FFmpeg process:`, err);
res.status(500).send('FFmpeg Prozess konnte nicht gestartet werden: ' + err.message);
});
} catch (error) {
console.error(`[${reqId}] ❌ Server error:`, error);
res.status(500).send('Interner Serverfehler: ' + error.message);
}
});
// Start Server
app.listen(PORT, '0.0.0.0', () => {
console.log(`
╔═══════════════════════════════════════════════════════════════╗
║ VIZ AUDIO VISUALIZER ║
║ DaVinci Resolve Alpha Edition v1.0.0 ║
╠═══════════════════════════════════════════════════════════════╣
║ 🌐 Web Interface: http://localhost:${PORT} ║
║ 📁 Data Directory: ${DATA_DIR}
║ 🎬 Render Support: ProRes 4444 Alpha / WebM / MP4 ║
╚═══════════════════════════════════════════════════════════════╝
`);
});