- Dynamische Modulerkennung via module.json in ModuleService.php - Kaskadierender Env-Loader fuer zentrale API-Keys (GEMINI_API_KEY) - Graceful Auth fuer alle Subprojekte (dinos, plants, storymachine, test) - Wiederverwendbares Starter-Template in _template/ - test-Modul auf philippurbschat.de Dark-Design modernisiert
186 lines
No EOL
9.6 KiB
PHP
186 lines
No EOL
9.6 KiB
PHP
<?php
|
||
require_once __DIR__ . '/bootstrap.php';
|
||
|
||
// Modul-Metadaten
|
||
$moduleDescriptor = file_exists(__DIR__ . '/module.json')
|
||
? json_decode(file_get_contents(__DIR__ . '/module.json'), true)
|
||
: [];
|
||
|
||
$moduleName = $moduleDescriptor['name'] ?? 'Test & Lab';
|
||
$moduleBadge = $moduleDescriptor['badge'] ?? 'Audio';
|
||
$moduleDesc = $moduleDescriptor['description'] ?? 'Modul-Showcase & Web Audio Synthesizer';
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title><?= htmlspecialchars($moduleName) ?> – philippurbschat.de</title>
|
||
|
||
<link rel="stylesheet" href="/css/app.css">
|
||
<link rel="stylesheet" href="/css/style.css">
|
||
|
||
<!-- Standalone-Fallback -->
|
||
<script>
|
||
window.addEventListener('error', function(e) {
|
||
if (e.target && e.target.tagName === 'LINK' && e.target.href.includes('/css/app.css')) {
|
||
const cdn = document.createElement('script');
|
||
cdn.src = 'https://cdn.tailwindcss.com';
|
||
document.head.appendChild(cdn);
|
||
}
|
||
}, true);
|
||
</script>
|
||
</head>
|
||
<body class="bg-slate-950 text-slate-200 min-h-screen font-sans flex flex-col antialiased selection:bg-cyan-500 selection:text-white">
|
||
|
||
<!-- Top Navigation Bar -->
|
||
<header class="border-b border-slate-800/80 bg-slate-900/50 backdrop-blur-md sticky top-0 z-50">
|
||
<div class="max-w-5xl mx-auto px-4 sm:px-6 py-3.5 flex items-center justify-between">
|
||
<div class="flex items-center gap-3">
|
||
<a href="/" class="text-xs font-mono text-slate-400 hover:text-cyan-400 transition-colors flex items-center gap-1.5 group">
|
||
<span class="group-hover:-translate-x-0.5 transition-transform">←</span> philippurbschat.de
|
||
</a>
|
||
<span class="text-slate-700">/</span>
|
||
<span class="text-xs font-mono text-slate-300 font-medium">test</span>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-[11px] font-mono px-2 py-0.5 rounded-full border border-cyan-500/30 bg-cyan-500/10 text-cyan-400 uppercase tracking-wider">
|
||
<?= htmlspecialchars($moduleBadge) ?>
|
||
</span>
|
||
<?php if ($isAdmin): ?>
|
||
<a href="/admin/users" class="text-[11px] font-mono px-2 py-0.5 rounded-full border border-fuchsia-500/30 bg-fuchsia-500/10 text-fuchsia-400 hover:bg-fuchsia-500/20 transition-colors uppercase tracking-wider">
|
||
Benutzer
|
||
</a>
|
||
<?php endif; ?>
|
||
<span class="text-[11px] font-mono text-slate-500 hidden sm:inline-block">
|
||
<?= htmlspecialchars($currentUser) ?>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Main Content -->
|
||
<main class="grow max-w-5xl mx-auto px-4 sm:px-6 py-10 w-full">
|
||
<!-- Header -->
|
||
<div class="mb-10">
|
||
<div class="inline-flex items-center gap-2 mb-3">
|
||
<span class="w-2 h-2 rounded-full bg-cyan-400 animate-pulse"></span>
|
||
<span class="text-xs font-mono uppercase tracking-[0.2em] text-cyan-400">Audio Lab</span>
|
||
</div>
|
||
<h1 class="text-3xl sm:text-4xl font-bold font-mono tracking-tight text-white mb-2">
|
||
<?= htmlspecialchars($moduleName) ?>
|
||
</h1>
|
||
<p class="text-slate-400 text-sm max-w-2xl">
|
||
<?= htmlspecialchars($moduleDesc) ?>
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Sandbox & Synth Card -->
|
||
<div id="synth-container" class="bg-slate-900/40 border border-slate-800 rounded-2xl p-6 sm:p-8 backdrop-blur shadow-2xl relative overflow-hidden transition-transform duration-200">
|
||
<div class="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-cyan-500/40 to-transparent"></div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||
<!-- Info Section -->
|
||
<div class="space-y-4">
|
||
<h2 class="text-xs font-mono uppercase tracking-widest text-slate-400">Web Audio API Test</h2>
|
||
<p class="text-sm text-slate-300 leading-relaxed">
|
||
Dieses Modul dient als Referenzimplementierung für das neue Modulsystem. Es demonstriert dynamische Web Audio Synthese (Oszillatoren, Rauschgeneratoren und Biquad-Filter) im Browser.
|
||
</p>
|
||
<div class="p-4 rounded-xl bg-slate-950/70 border border-slate-800/80 font-mono text-xs space-y-2">
|
||
<div class="text-slate-400">STATUS: <span id="audio-status" class="text-emerald-400">Bereit für Signaltest</span></div>
|
||
<div class="text-slate-500 text-[11px]">System: Graceful Auth • Auto-Discovered</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Interactive Trigger -->
|
||
<div class="flex flex-col items-center justify-center p-8 rounded-xl bg-slate-950/60 border border-slate-800/60 text-center">
|
||
<button id="trigger-sound" class="w-32 h-32 rounded-full bg-gradient-to-br from-cyan-500/20 to-fuchsia-500/20 border-2 border-cyan-500/50 hover:border-cyan-400 hover:shadow-[0_0_30px_rgba(6,182,212,0.3)] transition-all duration-300 flex flex-col items-center justify-center gap-2 group cursor-pointer active:scale-95 select-none">
|
||
<svg class="w-8 h-8 text-cyan-400 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
|
||
</svg>
|
||
<span class="text-[10px] font-mono uppercase tracking-widest text-slate-300 group-hover:text-white">Signal Starten</span>
|
||
</button>
|
||
<p class="text-xs font-mono text-slate-500 mt-4">Klicken zum Erzeugen eines synthetischen Klanges</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Footer -->
|
||
<footer class="border-t border-slate-900 py-6 text-center text-xs font-mono text-slate-600">
|
||
<?= htmlspecialchars($moduleName) ?> • Philipp Urbschat Lab
|
||
</footer>
|
||
|
||
<script>
|
||
const triggerBtn = document.getElementById('trigger-sound');
|
||
const synthContainer = document.getElementById('synth-container');
|
||
const statusMsg = document.getElementById('audio-status');
|
||
|
||
function playSynthesizerTone() {
|
||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||
|
||
const duration = 0.4 + Math.random() * 0.4;
|
||
const mainGain = audioCtx.createGain();
|
||
|
||
const osc = audioCtx.createOscillator();
|
||
const oscGain = audioCtx.createGain();
|
||
osc.type = 'sawtooth';
|
||
osc.frequency.setValueAtTime(55 + Math.random() * 25, audioCtx.currentTime);
|
||
osc.frequency.exponentialRampToValueAtTime(25, audioCtx.currentTime + duration);
|
||
|
||
const lfo = audioCtx.createOscillator();
|
||
const lfoGain = audioCtx.createGain();
|
||
lfo.type = 'square';
|
||
lfo.frequency.setValueAtTime(12 + Math.random() * 6, audioCtx.currentTime);
|
||
lfoGain.gain.setValueAtTime(0.7, audioCtx.currentTime);
|
||
lfo.connect(lfoGain);
|
||
lfoGain.connect(oscGain.gain);
|
||
|
||
const bufferSize = Math.floor(audioCtx.sampleRate * duration);
|
||
const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
||
const noiseData = noiseBuffer.getChannelData(0);
|
||
for (let i = 0; i < bufferSize; i++) {
|
||
noiseData[i] = Math.random() * 2 - 1;
|
||
}
|
||
const noise = audioCtx.createBufferSource();
|
||
noise.buffer = noiseBuffer;
|
||
const noiseFilter = audioCtx.createBiquadFilter();
|
||
noiseFilter.type = 'bandpass';
|
||
noiseFilter.frequency.setValueAtTime(380, audioCtx.currentTime);
|
||
|
||
const lowPass = audioCtx.createBiquadFilter();
|
||
lowPass.type = 'lowpass';
|
||
lowPass.frequency.setValueAtTime(420, audioCtx.currentTime);
|
||
lowPass.frequency.exponentialRampToValueAtTime(70, audioCtx.currentTime + duration);
|
||
|
||
mainGain.gain.setValueAtTime(0, audioCtx.currentTime);
|
||
mainGain.gain.linearRampToValueAtTime(0.4, audioCtx.currentTime + 0.05);
|
||
mainGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
|
||
|
||
osc.connect(oscGain);
|
||
oscGain.connect(lowPass);
|
||
noise.connect(noiseFilter);
|
||
noiseFilter.connect(lowPass);
|
||
lowPass.connect(mainGain);
|
||
mainGain.connect(audioCtx.destination);
|
||
|
||
osc.start();
|
||
lfo.start();
|
||
noise.start();
|
||
|
||
osc.stop(audioCtx.currentTime + duration);
|
||
lfo.stop(audioCtx.currentTime + duration);
|
||
noise.stop(audioCtx.currentTime + duration);
|
||
|
||
statusMsg.textContent = "Signal generiert (Synthese erfolgreich)";
|
||
}
|
||
|
||
triggerBtn.addEventListener('click', () => {
|
||
playSynthesizerTone();
|
||
synthContainer.classList.add('ring-1', 'ring-cyan-500/50');
|
||
setTimeout(() => synthContainer.classList.remove('ring-1', 'ring-cyan-500/50'), 300);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|