using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Threading; using TMPro; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; using UnityEngine.UI; public class SongCreatorManager : MonoBehaviour { [Header("음원 선택")] [SerializeField] private TMP_Dropdown audioDropdown; [SerializeField] private Button refreshBtn; [SerializeField] private TMP_Text inputPathHint; [Header("음원 추가 — 로컬 파일")] [SerializeField] private Button filePickerBtn; // 파일 탐색 버튼 [SerializeField] private TMP_Text addStatusText; // 추가/다운로드 상태 [Header("음원 추가 — URL")] [SerializeField] private TMP_InputField urlInput; // MP3 직접 URL [SerializeField] private Button urlDownloadBtn; // URL 다운로드 시작 [Header("메타데이터")] [SerializeField] private TMP_InputField titleInput; [SerializeField] private TMP_InputField artistInput; [SerializeField] private TMP_InputField bpmInput; [Header("난이도")] [SerializeField] private Toggle toggleNormal; [SerializeField] private Toggle toggleHard; [SerializeField] private Toggle toggleExpert; [SerializeField] private Toggle toggleExpertPlus; [Header("액션")] [SerializeField] private Button generateButton; [SerializeField] private Button manualEditorButton; // 작은 버튼 [SerializeField] private Button backButton; [SerializeField] private string introSceneName = "Intro"; [Header("진행 상태")] [SerializeField] private GameObject progressGroup; [SerializeField] private TMP_Text statusText; [SerializeField] private Slider progressSlider; [Header("연결")] [SerializeField] private BeatSageUploader beatSageUploader; [SerializeField] private NasPublisher nasPublisher; [Header("씬")] [SerializeField] private string mapEditorScene = "MapEditorScene"; // Quest: /sdcard/Android/data/{packageName}/files/input/ 로 ADB 복사 private static string InputPath => Path.Combine(Application.persistentDataPath, "input"); private readonly List audioFiles = new(); private string _pendingFilePath; // 파일 다이얼로그 결과 (백그라운드 스레드 → 메인 스레드 전달) // ── Unity ──────────────────────────────────────────────── private void Start() { Directory.CreateDirectory(InputPath); if (inputPathHint != null) inputPathHint.text = $"음원 경로: {InputPath}"; // 씬에서 아무 토글도 안 선택되어 있으면 전부 켜기 bool anyOn = (toggleNormal != null && toggleNormal.isOn) || (toggleHard != null && toggleHard.isOn) || (toggleExpert != null && toggleExpert.isOn) || (toggleExpertPlus != null && toggleExpertPlus.isOn); if (!anyOn) { if (toggleNormal != null) toggleNormal.isOn = true; if (toggleHard != null) toggleHard.isOn = true; if (toggleExpert != null) toggleExpert.isOn = true; if (toggleExpertPlus != null) toggleExpertPlus.isOn = true; } refreshBtn.onClick.AddListener(RefreshAudioList); generateButton.onClick.AddListener(OnGenerateClicked); manualEditorButton.onClick.AddListener(() => SceneManager.LoadScene(mapEditorScene)); backButton?.onClick.AddListener(() => SceneManager.LoadScene(introSceneName)); if (filePickerBtn != null) filePickerBtn.onClick.AddListener(OnFilePickerClicked); if (urlDownloadBtn != null) urlDownloadBtn.onClick.AddListener(OnUrlDownloadClicked); progressGroup.SetActive(false); RefreshAudioList(); } private void Update() { // 파일 다이얼로그는 STA 스레드에서 실행되므로 결과를 메인 스레드에서 처리 if (_pendingFilePath != null) { CopyToInput(_pendingFilePath); _pendingFilePath = null; } } // ── 음원 목록 갱신 ─────────────────────────────────────── private void RefreshAudioList() { audioFiles.Clear(); audioDropdown.ClearOptions(); string[] files = Directory.GetFiles(InputPath, "*.mp3"); var options = new List(); foreach (string f in files) { audioFiles.Add(f); options.Add(Path.GetFileNameWithoutExtension(f)); } if (options.Count == 0) options.Add("-- .mp3 파일 없음 --"); audioDropdown.AddOptions(options); } // ── 생성 버튼 ──────────────────────────────────────────── private void OnGenerateClicked() { if (audioFiles.Count == 0) { SetStatus("음원 파일이 없습니다."); return; } if (string.IsNullOrEmpty(titleInput.text)) { SetStatus("곡 제목을 입력해주세요."); return; } if (!float.TryParse(bpmInput.text, out float bpm) || bpm <= 0) { SetStatus("BPM을 올바르게 입력해주세요."); return; } var diffs = new List(); if (toggleNormal != null && toggleNormal.isOn) diffs.Add("normal"); if (toggleHard != null && toggleHard.isOn) diffs.Add("hard"); if (toggleExpert != null && toggleExpert.isOn) diffs.Add("expert"); if (toggleExpertPlus != null && toggleExpertPlus.isOn) diffs.Add("expertplus"); if (diffs.Count == 0) { SetStatus("난이도를 하나 이상 선택해주세요."); return; } string audioPath = audioFiles[audioDropdown.value]; Debug.Log($"[SongCreator] 생성 시작 — 파일: {audioPath}, BPM: {bpm}, 난이도: {string.Join(",", diffs)}"); Debug.Log($"[SongCreator] beatSageUploader={beatSageUploader}, nasPublisher={nasPublisher}"); StartCoroutine(GenerateFlow(audioPath, bpm, diffs)); } // ── 생성 플로우 ─────────────────────────────────────────── private IEnumerator GenerateFlow(string audioPath, float bpm, List diffs) { SetInteractable(false); progressGroup.SetActive(true); Debug.Log("[SongCreator] GenerateFlow 시작"); // 1단계: Beat Sage 전송 → 변환 Dictionary> maps = null; bool failed = false; Debug.Log("[SongCreator] BeatSage Upload 호출"); yield return beatSageUploader.Upload( audioPath, diffs, bpm, onProgress: p => { progressSlider.value = p * 0.8f; SetStatus($"{beatSageUploader.CurrentStatus} ({(int)(p * 80)}%)"); }, onComplete: result => { maps = result; Debug.Log($"[SongCreator] BeatSage 완료 — 난이도 수: {result?.Count}"); }, onError: err => { Debug.LogError($"[SongCreator] BeatSage 오류: {err}"); SetStatus($"오류: {err}"); failed = true; }); Debug.Log($"[SongCreator] BeatSage 단계 끝 — failed={failed}, maps={maps?.Count}"); if (failed) { SetInteractable(true); yield break; } // 2단계: NAS 업로드 SongInfo song = BuildSongInfo(audioPath, bpm, maps); Debug.Log($"[SongCreator] NAS Publish 호출 — song.id={song.id}"); yield return nasPublisher.Publish( song, audioPath, maps, onProgress: p => { progressSlider.value = 0.8f + p * 0.2f; SetStatus($"[4/4] NAS 업로드 중... ({(int)((0.8f + p * 0.2f) * 100)}%)"); }, onComplete: () => { progressSlider.value = 1f; SetStatus($"완료! '{song.title}' 생성 성공 (100%)"); Debug.Log($"[SongCreator] NAS 업로드 완료"); }, onError: err => { Debug.LogError($"[SongCreator] NAS 오류: {err}"); SetStatus($"NAS 업로드 실패: {err}"); failed = true; }); SetInteractable(true); } // ── 유틸 ───────────────────────────────────────────────── private SongInfo BuildSongInfo(string audioPath, float bpm, Dictionary> maps) { string id = titleInput.text.ToLower().Replace(" ", "_"); var diffMap = new DifficultyMap(); foreach (var kv in maps) { var info = new DifficultyInfo { noteCount = kv.Value.Count }; switch (kv.Key) { case "normal": diffMap.normal = info; break; case "hard": diffMap.hard = info; break; case "expert": diffMap.expert = info; break; case "expertplus": diffMap.expertplus = info; break; } } return new SongInfo { id = id, title = titleInput.text, artist = artistInput.text, bpm = bpm, audioFile = $"music/{id}.mp3", difficulties = diffMap, addedAt = DateTime.Now.ToString("yyyy-MM-dd") }; } private void SetStatus(string msg) { if (statusText != null) statusText.text = msg; } private void SetInteractable(bool value) { generateButton.interactable = value; manualEditorButton.interactable = value; audioDropdown.interactable = value; refreshBtn.interactable = value; if (filePickerBtn != null) filePickerBtn.interactable = value; if (urlDownloadBtn != null) urlDownloadBtn.interactable = value; } // ── 로컬 파일 선택 ──────────────────────────────────────── private void OnFilePickerClicked() { #if UNITY_EDITOR string path = UnityEditor.EditorUtility.OpenFilePanel("음원 파일 선택", "", "mp3"); if (!string.IsNullOrEmpty(path)) CopyToInput(path); #elif UNITY_STANDALONE_WIN var t = new Thread(() => { var dlg = new System.Windows.Forms.OpenFileDialog { Filter = "MP3 파일|*.mp3", Title = "음원 파일 선택" }; if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) _pendingFilePath = dlg.FileName; }); t.SetApartmentState(ApartmentState.STA); t.Start(); #else SetAddStatus($"ADB로 파일을 추가하세요:\n{InputPath}"); #endif } private void CopyToInput(string srcPath) { try { string dest = Path.Combine(InputPath, Path.GetFileName(srcPath)); File.Copy(srcPath, dest, overwrite: true); RefreshAudioList(); string nameNoExt = Path.GetFileNameWithoutExtension(srcPath); int idx = audioFiles.FindIndex(f => Path.GetFileNameWithoutExtension(f) == nameNoExt); if (idx >= 0) audioDropdown.value = idx; SetAddStatus($"추가됨: {Path.GetFileName(srcPath)}"); } catch (Exception e) { SetAddStatus($"파일 추가 실패: {e.Message}"); } } // ── URL 다운로드 ────────────────────────────────────────── private void OnUrlDownloadClicked() { string url = urlInput != null ? urlInput.text.Trim() : ""; if (string.IsNullOrEmpty(url)) { SetAddStatus("URL을 입력해주세요."); return; } StartCoroutine(DownloadFromUrl(url)); } private IEnumerator DownloadFromUrl(string url) { SetAddStatus("다운로드 중..."); if (urlDownloadBtn != null) urlDownloadBtn.interactable = false; string fileName; try { string uriPath = new Uri(url).AbsolutePath; fileName = Path.GetFileName(uriPath); if (string.IsNullOrEmpty(fileName) || !fileName.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase)) fileName = "download.mp3"; } catch { fileName = "download.mp3"; } string savePath = Path.GetFullPath(Path.Combine(InputPath, fileName)); using var req = UnityWebRequest.Get(url); req.downloadHandler = new DownloadHandlerFile(savePath); yield return req.SendWebRequest(); if (urlDownloadBtn != null) urlDownloadBtn.interactable = true; if (req.result == UnityWebRequest.Result.Success) { RefreshAudioList(); string nameNoExt = Path.GetFileNameWithoutExtension(fileName); int idx = audioFiles.FindIndex(f => Path.GetFileNameWithoutExtension(f) == nameNoExt); if (idx >= 0) audioDropdown.value = idx; SetAddStatus($"다운로드 완료: {fileName}"); } else { if (File.Exists(savePath)) File.Delete(savePath); SetAddStatus($"다운로드 실패: {req.error}"); } } private void SetAddStatus(string msg) { if (addStatusText != null) addStatusText.text = msg; } }