'use client' import { useState, useRef, useEffect, useCallback } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { Play, Pause, SkipForward, SkipBack, Volume2, VolumeX, X, } from 'lucide-react' import type { MediaItem } from '@/lib/types' // ─── helpers ──────────────────────────────────────────────────────────────── function formatTime(s: number) { if (!s || isNaN(s) || !isFinite(s)) return '--:--' const m = Math.floor(s / 60) const sec = Math.floor(s % 60) return `${m}:${sec.toString().padStart(2, '0')}` } function WaveformBars({ playing }: { playing: boolean }) { return (
{[0.55, 1, 0.7, 0.9, 0.5].map((h, i) => ( ))}
) } // ─── ambient audio via Web Audio API ──────────────────────────────────────── // A-minor chord: A1 55Hz · A2 110Hz · C3 130.81Hz · E3 164.81Hz · A3 220Hz function useAmbient() { const ctxRef = useRef(null) const stopFnsRef = useRef<(() => void)[]>([]) const [playing, setPlaying] = useState(false) const start = useCallback(() => { if (ctxRef.current) return const ctx = new AudioContext() ctxRef.current = ctx const master = ctx.createGain() master.gain.setValueAtTime(0, ctx.currentTime) master.gain.linearRampToValueAtTime(0.11, ctx.currentTime + 6) master.connect(ctx.destination) // Feedback delay for spaciousness const delay = ctx.createDelay(2.0) delay.delayTime.value = 1.4 const fbGain = ctx.createGain() fbGain.gain.value = 0.22 delay.connect(fbGain) fbGain.connect(delay) fbGain.connect(master) const notes = [ { freq: 55, gain: 0.55 }, { freq: 110, gain: 0.45 }, { freq: 130.81, gain: 0.38 }, { freq: 164.81, gain: 0.48 }, { freq: 220, gain: 0.30 }, ] notes.forEach(({ freq, gain }, i) => { const osc = ctx.createOscillator() osc.type = 'sine' osc.frequency.value = freq + (i % 2 === 0 ? 0.15 : -0.15) // micro-detune const g = ctx.createGain() g.gain.value = gain // Slow per-note swell LFO const lfo = ctx.createOscillator() lfo.type = 'sine' lfo.frequency.value = 0.06 + i * 0.018 const lfoG = ctx.createGain() lfoG.gain.value = gain * 0.38 lfo.connect(lfoG) lfoG.connect(g.gain) lfo.start() osc.connect(g) g.connect(master) g.connect(delay) osc.start() stopFnsRef.current.push(() => { try { osc.stop(); lfo.stop() } catch { /* already stopped */ } }) }) setPlaying(true) }, []) const stop = useCallback(() => { const ctx = ctxRef.current if (!ctx) return const master = ctx.destination // Fade out gracefully const g = ctx.createGain() g.gain.setValueAtTime(1, ctx.currentTime) g.gain.linearRampToValueAtTime(0, ctx.currentTime + 1.5) g.connect(master) setTimeout(() => { stopFnsRef.current.forEach((fn) => fn()) stopFnsRef.current = [] ctx.close() ctxRef.current = null setPlaying(false) }, 1600) }, []) const toggle = useCallback(() => { if (playing) stop() else start() }, [playing, start, stop]) return { playing, toggle, start, stop } } // ─── component ────────────────────────────────────────────────────────────── export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) { const [current, setCurrent] = useState(0) const [playing, setPlaying] = useState(false) const [volume, setVolume] = useState(0.4) const [muted, setMuted] = useState(false) const [progress, setProgress] = useState(0) const [duration, setDuration] = useState(0) const [elapsed, setElapsed] = useState(0) const [miniVisible, setMiniVisible] = useState(false) const audioRef = useRef(null) const ambient = useAmbient() const hasTrack = tracks.length > 0 const track = tracks[current] ?? null const trackName = (i: number) => tracks[i]?.original_name?.replace(/\.[^/.]+$/, '') || tracks[i]?.caption || `Titel ${i + 1}` useEffect(() => { const a = audioRef.current if (!a) return a.volume = muted ? 0 : volume }, [volume, muted]) useEffect(() => { const a = audioRef.current if (!a || !track) return a.src = `/api/files/${track.filename}` a.volume = muted ? 0 : volume setDuration(0); setElapsed(0); setProgress(0) if (playing) a.play().catch(() => setPlaying(false)) // eslint-disable-next-line react-hooks/exhaustive-deps }, [current]) const togglePlay = () => { const a = audioRef.current if (!a) return if (playing) a.pause() else a.play().catch(() => setPlaying(false)) } const playTrack = (i: number) => { if (i === current) togglePlay() else { setCurrent(i); setPlaying(true) } setMiniVisible(true) } const prev = () => setCurrent((c) => (c - 1 + tracks.length) % tracks.length) const next = () => setCurrent((c) => (c + 1) % tracks.length) const handleTimeUpdate = () => { const a = audioRef.current if (!a || !a.duration) return setProgress((a.currentTime / a.duration) * 100) setElapsed(a.currentTime) } const handleSeek = (e: React.ChangeEvent) => { const a = audioRef.current if (!a || !a.duration) return const pct = parseFloat(e.target.value) a.currentTime = (pct / 100) * a.duration setProgress(pct) } // Decide what the mini-player shows const miniLabel = hasTrack ? trackName(current) : 'Stille Begleitung' const miniPlaying = hasTrack ? playing : ambient.playing return ( <> {hasTrack && (