import React, { useCallback, useEffect, useRef, useState } from 'react'; import { deleteMusicTrack, generateMusic, generateMusicLyrics, getMusicLibrary, getMusicProviders, getMusicStatus, getMusicModels, getMusicCredits, extendMusicTrack, removeVocals, } from '../../api'; import './MusicStudio.css'; /* ───────────────────────────────────────────── 데이터 상수 ───────────────────────────────────────────── */ const GENRES = [ { id: 'lofi', label: 'Lo-Fi', icon: '📻', color: '#f5a623', desc: 'Warm · Nostalgic' }, { id: 'electronic', label: 'Electronic', icon: '⚡', color: '#60a5fa', desc: 'Pulse · Synth' }, { id: 'jazz', label: 'Jazz', icon: '🎷', color: '#c084fc', desc: 'Smooth · Soul' }, { id: 'classical', label: 'Classical', icon: '🎻', color: '#f9b6b1', desc: 'Orchestral · Grand' }, { id: 'ambient', label: 'Ambient', icon: '🌊', color: '#4aad8b', desc: 'Space · Float' }, { id: 'hiphop', label: 'Hip-Hop', icon: '🎤', color: '#f472b6', desc: 'Beat · Urban' }, { id: 'rock', label: 'Rock', icon: '🎸', color: '#e85c3a', desc: 'Raw · Drive' }, { id: 'cinematic', label: 'Cinematic', icon: '🎬', color: '#fbbf24', desc: 'Epic · Sweeping' }, ]; const MOODS = [ { id: 'energetic', label: 'Energetic', color: '#e85c3a' }, { id: 'chill', label: 'Chill', color: '#60a5fa' }, { id: 'dark', label: 'Dark', color: '#9333ea' }, { id: 'uplifting', label: 'Uplifting', color: '#f5a623' }, { id: 'romantic', label: 'Romantic', color: '#f472b6' }, { id: 'epic', label: 'Epic', color: '#fbbf24' }, { id: 'melancholic', label: 'Melancholic', color: '#4aad8b' }, ]; const INSTRUMENTS = [ { id: 'piano', label: 'Piano', freq: '261Hz' }, { id: 'guitar', label: 'Guitar', freq: '82Hz' }, { id: 'drums', label: 'Drums', freq: '60Hz' }, { id: 'synth', label: 'Synth', freq: '440Hz' }, { id: 'bass', label: 'Bass', freq: '41Hz' }, { id: 'strings', label: 'Strings', freq: '196Hz' }, { id: 'brass', label: 'Brass', freq: '146Hz' }, { id: 'flute', label: 'Flute', freq: '523Hz' }, { id: 'violin', label: 'Violin', freq: '659Hz' }, { id: 'choir', label: 'Choir', freq: '330Hz' }, ]; const DURATIONS = [ { id: '30s', label: '0:30', sec: 30 }, { id: '60s', label: '1:00', sec: 60 }, { id: '90s', label: '1:30', sec: 90 }, { id: '2m', label: '2:00', sec: 120 }, { id: '3m', label: '3:00', sec: 180 }, { id: '5m', label: '5:00', sec: 300 }, ]; const BPM_PRESETS = [ { label: 'Slow', bpm: 70 }, { label: 'Mid', bpm: 100 }, { label: 'Fast', bpm: 130 }, { label: 'EDM', bpm: 160 }, ]; const KEYS = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']; const SCALES = ['Major','Minor','Dorian','Phrygian','Lydian','Mixolydian']; /* 시뮬레이션 폴백용 단계 메시지 */ const SIM_STEPS = [ { msg: 'Analyzing musical patterns…', pct: 16 }, { msg: 'Building harmonic structure…', pct: 32 }, { msg: 'Rendering instruments…', pct: 52 }, { msg: 'Mixing and mastering…', pct: 72 }, { msg: 'Applying final polish…', pct: 90 }, { msg: 'Track ready!', pct: 100 }, ]; /* ───────────────────────────────────────────── 유틸 ───────────────────────────────────────────── */ const pad = (n) => String(Math.floor(n)).padStart(2, '0'); const fmtTime = (s) => `${pad(s / 60)}:${pad(s % 60)}`; /* ───────────────────────────────────────────── Loading Skeleton ───────────────────────────────────────────── */ const SkeletonCard = () => (
); /* ───────────────────────────────────────────── Waveform Canvas ───────────────────────────────────────────── */ const WaveformCanvas = ({ isGenerating, accentColor }) => { const canvasRef = useRef(null); const rafRef = useRef(null); const phaseRef = useRef(0); const intensityRef = useRef(0); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); let dpr = window.devicePixelRatio || 1; const resize = () => { dpr = window.devicePixelRatio || 1; canvas.width = canvas.offsetWidth * dpr; canvas.height = canvas.offsetHeight * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; resize(); const ro = new ResizeObserver(resize); ro.observe(canvas); const draw = () => { const w = canvas.offsetWidth, h = canvas.offsetHeight; ctx.clearRect(0, 0, w, h); const target = isGenerating ? 1.0 : 0.25; intensityRef.current += (target - intensityRef.current) * 0.04; const intensity = intensityRef.current; const layers = [ { amp: h * 0.38 * intensity, freq: 1.6, speed: 0.022, alpha: 0.9, lw: 2 }, { amp: h * 0.22 * intensity, freq: 2.8, speed: 0.034, alpha: 0.5, lw: 1.2 }, { amp: h * 0.12 * intensity, freq: 4.5, speed: 0.055, alpha: 0.3, lw: 0.8 }, ]; layers.forEach(({ amp, freq, speed, alpha, lw }, li) => { ctx.beginPath(); ctx.strokeStyle = accentColor; ctx.globalAlpha = alpha; ctx.lineWidth = lw; ctx.shadowBlur = isGenerating ? 16 : 6; ctx.shadowColor = accentColor; for (let x = 0; x <= w; x += 2) { const t = (x / w) * Math.PI * 2 * freq + phaseRef.current * (1 + li * speed * 10); const h2 = Math.sin(t * 2.1 + li) * 0.35; const h3 = Math.sin(t * 0.45 + li * 0.7) * 0.18; const y = h / 2 + (Math.sin(t) + h2 + h3) * amp; x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); } ctx.stroke(); }); ctx.globalAlpha = 1; ctx.shadowBlur = 0; phaseRef.current += isGenerating ? 0.055 : 0.018; rafRef.current = requestAnimationFrame(draw); }; draw(); return () => { ro.disconnect(); cancelAnimationFrame(rafRef.current); }; }, [isGenerating, accentColor]); return ; }; /* ───────────────────────────────────────────── Sonic Radar (헤더 비주얼 — 모듈 로드 시 1회 계산) ───────────────────────────────────────────── */ const RADAR_N = 48; const RADAR_INNER = 26; // 중심~바 시작 거리(SVG unit) const RADAR_DATA = Array.from({ length: RADAR_N }, (_, i) => ({ angle: `${((i / RADAR_N) * 360).toFixed(2)}deg`, delay: `${((i / RADAR_N) * 1.8).toFixed(2)}s`, rnd: `${(0.22 + Math.random() * 0.78).toFixed(2)}`, })); const SonicRadar = ({ isGenerating, accentColor }) => (
{/* SVG — 링·크로스헤어·스윕 */} {/* 가이드 링 3개 */} {/* 크로스헤어 틱 마크 */} {/* 대각선 코너 틱 */} {/* 레이더 스윕 라인 (generating 시 회전) */} {/* 센터 글로우 링 */} {/* CSS 방사형 바 (HTML div) */} {RADAR_DATA.map((bar, i) => (
))} {/* 센터 도트 */}
{/* 상태 레이블 */}
{isGenerating ? 'GENERATING' : 'STANDBY'}
); /* ───────────────────────────────────────────── Progress Bar ───────────────────────────────────────────── */ const GenerationProgress = ({ progress, stepMsg }) => (
{stepMsg} {progress}%
); /* ───────────────────────────────────────────── Audio Player (실제