'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 && (