feat: initial commit for Viz audio visualizer studio
This commit is contained in:
commit
9bc7b188a8
10 files changed
+4433
No files matched your search
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
data/
|
||||
uploads/
|
||||
temp/
|
||||
.env
|
||||
.env.local
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -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.
|
||||
@@ -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
|
||||
Generated
+1018
File diff suppressed because it is too large.
Load diff
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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>
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+252
@@ -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 ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
`);
|
||||
});
|
||||
Reference in new issue
Block a user