eldouma-graphics / src /components /MusicLibrary.tsx
Moeeldouma's picture
Deploy Eldouma Graphics β€” Docker Space, bring-your-own-key demo
39d98a0 verified
Raw
History Blame Contribute Delete
10.3 kB
import { useRef, useState, useEffect } from "react";
import { Play, Pause, Check, Volume2, Music2, X, Waves, Settings2 } from "lucide-react";
import { MG_MUSIC, SFX_STYLES } from "../remotion/mg/catalog.mjs";
import { useStore } from "../store";
type Track = { id: string; label: string; file: string; genre: string; mood: string; bpm: number; durationSec: number; blurb: string; gradient: string };
const TRACKS = MG_MUSIC as Track[];
type SfxStyle = { id: string; label: string; icon: string; blurb: string };
const STYLES = SFX_STYLES as SfxStyle[];
// a representative cue to preview each style (so the chip "sounds like" what it does)
const STYLE_DEMO: Record<string, string> = { designed: "swoosh", cinematic: "whoosh", clean: "pop", punchy: "impact", soft: "chime", retro: "glitch", off: "" };
// a deterministic static "waveform" silhouette for a card at rest (no audio analysis needed)
const restBar = (i: number) => 22 + (Math.sin(i * 1.7) * 0.5 + 0.5) * 64 + ((i * 41) % 23);
// The Background-music panel β€” browse the library, preview any track, pick one (or "No music").
// The chosen track loops softly under the whole video (wired in EditorComposition).
export function MusicLibrary() {
const music = useStore((s) => s.music);
const musicVolume = useStore((s) => s.musicVolume);
const setMusic = useStore((s) => s.setMusic);
const setMusicVolume = useStore((s) => s.setMusicVolume);
const musicFadeInMs = useStore((s) => s.musicFadeInMs);
const musicFadeOutMs = useStore((s) => s.musicFadeOutMs);
const setMusicFade = useStore((s) => s.setMusicFade);
const sfxStyle = useStore((s) => s.sfxStyle);
const setSfxStyle = useStore((s) => s.setSfxStyle);
const [preview, setPreview] = useState<string | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
const sfxRef = useRef<HTMLAudioElement | null>(null);
const playSfx = (file: string) => { if (!file) return; try { sfxRef.current?.pause(); sfxRef.current = new Audio("/sfx/" + file + ".mp3"); sfxRef.current.volume = 0.85; void sfxRef.current.play(); } catch { /* ignore */ } };
const stop = () => { audioRef.current?.pause(); audioRef.current = null; setPreview(null); };
const togglePreview = (m: Track) => {
if (preview === m.id) { stop(); return; }
audioRef.current?.pause();
const a = new Audio("/music/" + m.file);
a.loop = true; a.volume = 0.9;
a.play().catch(() => { /* autoplay blocked until a gesture β€” this IS a gesture, so fine */ });
a.onended = () => setPreview(null);
audioRef.current = a; setPreview(m.id);
};
useEffect(() => () => { audioRef.current?.pause(); }, []);
const selected = TRACKS.find((m) => m.id === music) || null;
return (
<div className="flex h-full flex-col">
<style>{`@keyframes elgeq{0%,100%{transform:scaleY(0.28)}50%{transform:scaleY(1)}}.elg-eq{transform-origin:bottom;animation:elgeq .75s ease-in-out infinite}`}</style>
<div className="px-3 pt-3 pb-2">
<div className="flex items-center gap-1.5 text-[12px] font-semibold text-fg"><Waves size={13} className="text-primary" /> Sound &amp; Music</div>
<div className="mt-0.5 text-[10px] text-fg-subtle">Control every scene's sound effects and the background track in one place.</div>
</div>
<div className="flex-1 space-y-2 overflow-y-auto px-3 pb-3">
{/* ── Motion-graphics SOUND STYLE β€” re-skins every scene's entrance sound at once ── */}
<div className="rounded-xl border border-border bg-white/[0.02] p-2.5">
<div className="mb-1 flex items-center gap-1.5 text-[11px] font-semibold text-fg"><Settings2 size={12} className="text-primary" /> Motion sound</div>
<div className="mb-2 text-[10px] text-fg-subtle">Every scene plays a sound on its entrance. Pick a style to re-skin them all β€” tap to hear it.</div>
<div className="grid grid-cols-2 gap-1.5">
{STYLES.map((s) => {
const on = (sfxStyle || "designed") === s.id;
return (
<button key={s.id} onClick={() => { setSfxStyle(s.id); playSfx(STYLE_DEMO[s.id]); }} title={s.blurb}
className={"flex items-center gap-1.5 rounded-lg border px-2 py-1.5 text-left transition " + (on ? "border-primary bg-primary/10" : "border-border hover:border-primary/50")}>
<span className="text-[14px] leading-none">{s.icon}</span>
<span className="min-w-0">
<span className="block truncate text-[11px] font-medium text-fg">{s.label}</span>
<span className="block truncate text-[9px] text-fg-subtle">{s.blurb}</span>
</span>
{on && <Check size={12} className="ml-auto shrink-0 text-primary" />}
</button>
);
})}
</div>
<div className="mt-2 text-[9.5px] text-fg-subtle">Fine-tune one scene: select it on the timeline β†’ the inspector's <span className="text-fg-muted">Sound</span> section (or click its πŸ”Š badge).</div>
</div>
{/* ── BACKGROUND MUSIC ── */}
<div className="flex items-center gap-1.5 pt-1 text-[11px] font-semibold text-fg"><Music2 size={12} className="text-primary" /> Background music <span className="text-[9px] font-normal text-fg-subtle">β€” loops under the video</span></div>
{/* No music */}
<button onClick={() => setMusic("")}
className={"flex w-full items-center gap-2 rounded-lg border px-3 py-2 text-left transition " + (!music ? "border-primary bg-primary/10" : "border-border hover:border-primary/50")}>
<X size={14} className="text-fg-muted" /><span className="text-[12px] text-fg">No music</span>
{!music && <Check size={13} className="ml-auto text-primary" />}
</button>
{TRACKS.map((m) => {
const isSel = music === m.id, isPlaying = preview === m.id;
return (
<div key={m.id} onClick={() => setMusic(m.id)} title={`Use β€œ${m.label}” β€” or drag onto the timeline`}
draggable onDragStart={(e) => { e.dataTransfer.setData("application/x-elg-music", m.id); e.dataTransfer.effectAllowed = "copy"; }}
className={"group relative cursor-grab overflow-hidden rounded-xl border transition active:cursor-grabbing " + (isSel ? "border-primary ring-1 ring-primary/50" : "border-border hover:border-primary/60")}>
<div className="absolute inset-0" style={{ background: m.gradient }} />
<div className="absolute inset-0 bg-black/40 transition group-hover:bg-black/30" />
<div className="relative p-3">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="flex items-center gap-1.5 truncate text-[13px] font-semibold text-white drop-shadow-sm">
{m.label}{isSel && <Check size={13} className="shrink-0 text-white" />}
</div>
<div className="truncate text-[10px] text-white/85">{m.genre} Β· {m.mood}</div>
</div>
<button onClick={(e) => { e.stopPropagation(); togglePreview(m); }} title={isPlaying ? "Stop preview" : "Preview"}
className="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-white/95 text-black shadow-lg transition hover:scale-105 hover:bg-white">
{isPlaying ? <Pause size={15} /> : <Play size={15} className="ml-0.5" />}
</button>
</div>
{/* live equalizer / waveform */}
<div className="mt-2.5 flex h-6 items-end gap-[2.5px]">
{Array.from({ length: 32 }).map((_, i) => (
<span key={i} className={"flex-1 rounded-sm " + (isPlaying ? "elg-eq bg-white/85" : "bg-white/55")}
style={isPlaying ? { height: "100%", animationDelay: `${(i % 9) * 70}ms`, animationDuration: `${0.6 + (i % 5) * 0.1}s` } : { height: `${restBar(i)}%` }} />
))}
</div>
<div className="mt-2 flex items-center justify-between gap-2 text-[10px] text-white/85">
<span className="truncate">{m.blurb}</span>
<span className="shrink-0 tabular-nums">{m.bpm} BPM Β· {m.durationSec}s</span>
</div>
</div>
</div>
);
})}
</div>
{/* volume + status footer (only when a track is chosen) */}
{selected && (
<div className="border-t border-border px-3 py-2.5">
<div className="flex items-center gap-2">
<Volume2 size={14} className="shrink-0 text-fg-muted" />
<input type="range" min={0} max={1} step={0.05} value={musicVolume} onChange={(e) => setMusicVolume(Number(e.target.value))} className="h-1 flex-1 accent-primary" />
<span className="w-7 text-right text-[10px] tabular-nums text-fg-muted">{Math.round(musicVolume * 100)}</span>
</div>
<div className="mt-2 grid grid-cols-2 gap-x-3 gap-y-1">
<div className="flex items-center gap-1.5"><span className="w-12 shrink-0 text-[10px] text-fg-subtle">Fade in</span><input type="range" min={0} max={4000} step={100} value={musicFadeInMs} onChange={(e) => setMusicFade(Number(e.target.value), musicFadeOutMs)} className="h-1 flex-1 accent-primary" /><span className="w-7 shrink-0 text-right text-[10px] tabular-nums text-fg-muted">{(musicFadeInMs / 1000).toFixed(1)}s</span></div>
<div className="flex items-center gap-1.5"><span className="w-12 shrink-0 text-[10px] text-fg-subtle">Fade out</span><input type="range" min={0} max={4000} step={100} value={musicFadeOutMs} onChange={(e) => setMusicFade(musicFadeInMs, Number(e.target.value))} className="h-1 flex-1 accent-primary" /><span className="w-7 shrink-0 text-right text-[10px] tabular-nums text-fg-muted">{(musicFadeOutMs / 1000).toFixed(1)}s</span></div>
</div>
<div className="mt-1.5 truncate text-[10px] text-fg-subtle"><span className="text-fg-muted">{selected.label}</span> loops under your video, fading in &amp; out. Pick β€œNo music” to remove.</div>
</div>
)}
</div>
);
}