Files
homeclaw/web-ui/html/music-app.html
T
kimandClaude Opus 5 52a42e26f0 v4.3.36: 거대 HTML 3개의 인라인 JS 외부화 + 아두이노 페이지의 기존 버그 2건 수정
index.html(인라인 0%)과 code.html이 이미 쓰던 방식을 나머지 큰 페이지에 적용.
각 파일이 거대 인라인 <script> 하나씩이라 순수 이동으로 처리됨.

  pptx-wizard.html       3,988 →   820줄   (/js/pptx/pptx-wizard.js       3,174줄)
  music-app.html         4,549 → 1,197줄   (/js/music/music-app.js        3,360줄)
  arduino-emulator.html  8,026 →   849줄   (/js/arduino/arduino-emulator.js 7,186줄)

보존한 것:
- arduino의 type="module" 유지. 모듈 스코프에서는 최상위 선언이 전역이 되지
  않으므로, onclick 핸들러가 의존하는 window.foo= 노출 방식이 그대로 성립해야 함
- music-app의 테마 설정 스크립트는 인라인 유지 — 페인트 전에 실행돼야 하고
  외부화하면 네트워크 왕복 때문에 다크/라이트가 깜빡임
- 로드 위치는 셋 다 원래대로 </body> 직전
- document.currentScript / document.write / import.meta 사용 0건 확인
  (인라인과 외부의 의미가 달라지는 패턴 없음)

브라우저 실측 검증 중 아두이노 페이지에서 기존 버그 2건 발견 — 이동이 원인이
아니라 원래 깨져 있던 것이 파일 위치가 바뀌며 드러남:

1) 동적 import 경로가 처음부터 틀려 있었음
   import('./vendor/avr8js/index.js')는 /html/vendor/... 를 가리키는데 실제
   파일은 /vendor/... 에 있음. 서버에 SPA 캐치올이 있어 없는 경로도 200 + HTML을
   반환하는 탓에 404로 드러나지 않았고, JS 대신 HTML을 받아 import가 실패했음.
   절대경로 /vendor/... 로 수정(avr8js, rp2040js 2건).

2) 실패 핸들러가 2차 예외를 던져 모듈 전체를 중단시킴
   위 import 실패의 catch에서 showOverlayError()를 부르는데, 이 함수는 파일
   2874줄에서 window에 할당되므로 그 시점에는 아직 없음 → ReferenceError로 모듈
   실행이 통째로 멈추고, 아래쪽 window.* 노출이 전혀 일어나지 않아 onclick
   핸들러 100개가 전부 죽어 있었음. 로드 실패보다 이 2차 실패가 훨씬 치명적.
   console.error로 먼저 남기고 showOverlayError는 있을 때만 호출하도록 변경.

3) 전수 점검 중 window 노출이 누락된 핸들러 6개 발견(mobCloseBoard,
   mobToggleBoard, _wifi* 4개) — 클릭해도 ReferenceError만 나던 상태. 명시적 노출 추가.

브라우저 실측(192.168.0.5:18789):
  pptx-wizard  전역 함수 6/6, onclick 87개, 콘솔 에러 없음
  music-app    전역 함수 6/6, onclick 103개, 인라인 테마 정상(data-theme=dark)
  arduino      onclick 이름 해석 0/35 → 35/35, avr8js 로드 성공, 콘솔 깨끗

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:19:36 +09:00

1198 lines
80 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🎵 음악 스튜디오</title>
<link rel="icon" type="image/png" sizes="64x64" href="/homeclaw_logo.png">
<link rel="stylesheet" href="../css/styles.css">
<script src="https://cdn.jsdelivr.net/npm/@coderline/alphatab@latest/dist/alphaTab.min.js"></script>
<script>
try { var _t=localStorage.getItem('homeclaw_music_theme')||'dark'; document.documentElement.setAttribute('data-theme',_t); } catch(e){}
</script>
<style>
/* ── Premium studio theme tokens ── */
:root{
--studio-bg: #141520;
--studio-surface: #1b1c2c;
--studio-panel: #1f2032;
--studio-panel2: #272840;
--studio-border: rgba(255,255,255,.09);
--studio-border2: rgba(255,255,255,.16);
--studio-text: #d8daf0;
--studio-muted: #6e7096;
--studio-accent: #9333ea;
--studio-accent2: #a855f7;
--studio-glow: rgba(147,51,234,.35);
--studio-glow2: rgba(168,85,247,.15);
--studio-green: #22c55e;
--studio-red: #ef4444;
/* Remap global tokens */
--bg: #141520;
--panel: #1f2032;
--panel-2: #272840;
--line: rgba(255,255,255,.16);
--text: #d8daf0;
--muted: #6e7096;
--brand: #a855f7;
}
:root[data-theme="dark"]{
--bg: #141520;
--panel: #1f2032;
--panel-2: #272840;
--line: rgba(255,255,255,.16);
--text: #d8daf0;
--muted: #6e7096;
--brand: #a855f7;
}
:root[data-theme="light"]{
--studio-bg: #dddaf0;
--studio-surface: #e4e1f5;
--studio-panel: #eceaf8;
--studio-panel2: #e0ddf0;
--studio-border: rgba(0,0,0,.09);
--studio-border2: rgba(0,0,0,.17);
--studio-text: #1a1830;
--studio-muted: #6a6888;
--studio-glow: rgba(147,51,234,.2);
--studio-glow2: rgba(168,85,247,.1);
--bg: #dddaf0;
--panel: #eceaf8;
--panel-2: #e0ddf0;
--line: rgba(0,0,0,.17);
--text: #1a1830;
--muted: #6a6888;
}
html,body{height:100%;margin:0;padding:0;}
body{
background:var(--studio-bg);
color:var(--studio-text);
font-family:'Inter',system-ui,-apple-system,sans-serif;
display:flex;flex-direction:column;overflow:hidden;height:100dvh;
}
/* ── Header ── */
.ms-hdr{
display:flex;align-items:center;gap:10px;padding:0 16px;height:44px;
background:var(--studio-panel);
border-bottom:1px solid var(--studio-border);
flex-shrink:0;min-width:0;
box-shadow:0 1px 0 var(--studio-border);
}
.ms-hdr h1{font-size:13px;font-weight:700;margin:0;letter-spacing:.02em;background:linear-gradient(135deg,#c084fc,#818cf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;}
.ms-hdr-spacer{flex:1;}
.ms-hdr-btn{
background:transparent;border:1px solid var(--studio-border2);border-radius:7px;
padding:4px 11px;font-size:11px;color:var(--studio-muted);cursor:pointer;
text-decoration:none;display:inline-flex;align-items:center;transition:.2s;font-family:inherit;
letter-spacing:.02em;
}
.ms-hdr-btn:hover{border-color:var(--studio-accent2);color:var(--studio-accent2);background:var(--studio-glow2);}
.skill-badge{font-size:11px;padding:3px 10px;border-radius:20px;background:linear-gradient(135deg,rgba(147,51,234,.2),rgba(99,102,241,.2));border:1px solid rgba(168,85,247,.4);color:#c084fc;font-weight:700;letter-spacing:.03em;}
/* ── Layout ── */
.ms-layout{flex:1;display:flex;min-height:0;overflow:hidden;}
/* ── Sidebar ── */
.ms-sb{
width:208px;flex-shrink:0;display:flex;flex-direction:column;
border-right:1px solid var(--studio-border);
background:var(--studio-panel);
min-height:0;overflow:hidden;
}
.ms-sb-sec{
font-size:9.5px;font-weight:700;color:var(--studio-muted);
text-transform:uppercase;letter-spacing:.1em;padding:10px 12px 5px;
}
.ms-quick{padding:0 8px 8px;display:flex;flex-direction:column;gap:4px;flex-shrink:0;}
.ms-qbtn{
background:transparent;border:1px solid var(--studio-border2);border-radius:8px;
padding:6px 10px;font-size:11px;color:var(--studio-muted);cursor:pointer;
font-family:inherit;text-align:left;transition:.2s;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.ms-qbtn:hover{border-color:var(--studio-accent2);color:var(--studio-text);background:var(--studio-glow2);}
.ms-divider{height:1px;background:var(--studio-border);margin:5px 0;}
.ms-file-hdr{display:flex;align-items:center;padding:0 12px 4px;flex-shrink:0;}
.ms-file-hdr span{flex:1;font-size:9.5px;font-weight:700;color:var(--studio-muted);text-transform:uppercase;letter-spacing:.1em;}
.ms-file-hdr button{background:none;border:none;padding:0;cursor:pointer;font-size:12px;color:var(--studio-muted);transition:.15s;}
.ms-file-hdr button:hover{color:var(--studio-accent2);}
.ms-files{flex:1;overflow-y:auto;padding:0 6px;}
.ms-fi{display:flex;align-items:center;gap:5px;padding:4px 7px;border-radius:7px;cursor:pointer;font-size:11px;color:var(--studio-muted);transition:.15s;}
.ms-fi:hover{background:var(--studio-glow2);color:var(--studio-text);}
.ms-fi-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ms-fi-btn{background:none;border:none;cursor:pointer;font-size:12px;padding:2px 4px;color:var(--studio-muted);flex-shrink:0;border-radius:4px;transition:.15s;}
.ms-fi-btn:hover.play{color:var(--studio-green);}
.ms-fi-btn:hover.del{color:var(--studio-red);}
.ms-upload{
margin:8px;border:1px dashed var(--studio-border2);border-radius:8px;
padding:7px;font-size:11px;color:var(--studio-muted);cursor:pointer;
text-align:center;background:none;font-family:inherit;transition:.2s;
display:block;width:calc(100% - 16px);box-sizing:border-box;
}
.ms-upload:hover{border-color:var(--studio-accent2);color:var(--studio-accent2);background:var(--studio-glow2);}
/* ── Guitar Pro viewer ── */
#stage-gp{display:none;flex-direction:column;flex:1;min-height:0;overflow:hidden;}
.gp-bar{
display:flex;align-items:center;gap:8px;padding:7px 14px;
border-bottom:1px solid var(--studio-border);
background:var(--studio-panel);flex-shrink:0;
}
.gp-bar-title{font-size:12px;font-weight:700;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:.02em;}
.gp-ctrl-btn{
background:transparent;border:1px solid var(--studio-border2);border-radius:6px;
padding:3px 10px;font-size:11px;color:var(--studio-muted);cursor:pointer;
font-family:inherit;transition:.2s;
}
.gp-ctrl-btn:hover{border-color:var(--studio-accent2);color:var(--studio-accent2);background:var(--studio-glow2);}
.gp-ctrl-btn.active{background:var(--studio-accent);border-color:var(--studio-accent);color:#fff;box-shadow:0 0 10px var(--studio-glow);}
#gp-tempo{background:transparent;border:none;border-bottom:1px solid var(--studio-border2);color:var(--studio-text);font-size:13px;font-family:inherit;padding:1px 2px;text-align:center;outline:none;width:44px;}
#gp-tempo:focus{border-bottom-color:var(--studio-accent2);}
#gp-tempo::-webkit-inner-spin-button,#gp-tempo::-webkit-outer-spin-button{-webkit-appearance:none;margin:0;}
#gp-tempo[type=number]{-moz-appearance:textfield;}
.gp-viewport{flex:1;overflow:auto;background:var(--studio-bg);}
.at-main{padding:16px;position:relative;}
[data-theme="dark"] .at-main svg text{fill:#d1d5db !important;}
[data-theme="dark"] .at-main svg line,[data-theme="dark"] .at-main svg path{stroke:#4b5563 !important;}
.gp-track-list{display:flex;flex-wrap:wrap;gap:5px;padding:7px 14px;border-bottom:1px solid var(--studio-border);background:var(--studio-surface);flex-shrink:0;}
.gp-track-chip{
font-size:11px;padding:4px 10px 4px 12px;border-radius:999px;
border:1px solid var(--studio-border2);cursor:pointer;
background:var(--studio-panel2);color:var(--studio-muted);
transition:.2s;user-select:none;display:inline-flex;align-items:center;gap:6px;
}
.gp-track-chip.on{background:linear-gradient(135deg,var(--studio-accent),#7c3aed);border-color:var(--studio-accent);color:#fff;box-shadow:0 0 8px var(--studio-glow);}
.gp-track-chip:hover{border-color:var(--studio-accent2);color:var(--studio-text);}
.gp-track-chip.on:hover{opacity:.88;}
.gp-mute-btn{font-size:13px;line-height:1;cursor:pointer;opacity:.55;transition:.15s;flex-shrink:0;}
.gp-mute-btn:hover{opacity:1;}
.gp-mute-btn.muted{opacity:1;color:#f87171;}
.gp-track-chip .gp-vol{width:54px;cursor:pointer;accent-color:var(--studio-accent2);}
.gp-track-chip .gp-vol-val{font-size:10px;opacity:.7;min-width:22px;text-align:right;font-variant-numeric:tabular-nums;}
.gp-fx-e{display:inline-flex;align-items:center;gap:5px;color:var(--studio-text);cursor:pointer;white-space:nowrap;}
.gp-fx-val{font-size:10px;color:var(--studio-muted);min-width:24px;text-align:right;font-variant-numeric:tabular-nums;}
#gp-fx-panel .gp-vol{width:70px;accent-color:var(--studio-accent2);}
.gp-empty{flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:10px;color:var(--studio-muted);}
.gp-loading{font-size:12px;color:var(--studio-muted);text-align:center;padding:40px;}
.gp-fx-bar{display:flex;align-items:center;gap:10px;padding:5px 14px;border-bottom:1px solid var(--studio-border);background:var(--studio-surface);flex-shrink:0;}
.gp-fx-label{font-size:10px;color:var(--studio-muted);white-space:nowrap;letter-spacing:.04em;}
/* AlphaTab cursor */
.at-cursor-bar{background:rgba(147,51,234,.12);border-left:2px solid var(--studio-accent2);}
.at-cursor-beat{background:rgba(168,85,247,.55);width:2px;}
.at-highlight .at-note-head{fill:var(--studio-accent2) !important;}
/* ── Stage ── */
.ms-stage{flex:1;min-width:0;display:flex;flex-direction:column;overflow:hidden;background:var(--studio-bg);}
#audiomass-panel{flex:1;min-height:0;flex-direction:column;}
.ms-stage-hdr{
display:flex;align-items:center;padding:7px 14px;
border-bottom:1px solid var(--studio-border);
background:var(--studio-panel);flex-shrink:0;gap:8px;
box-shadow:0 1px 0 var(--studio-border);
}
.ms-stage-title{font-size:12px;font-weight:700;color:var(--studio-text);flex:1;letter-spacing:.03em;}
.ms-stage-btn{
background:transparent;border:1px solid var(--studio-border2);border-radius:6px;
padding:3px 9px;font-size:11px;color:var(--studio-muted);cursor:pointer;font-family:inherit;transition:.2s;
}
.ms-stage-btn:hover{border-color:var(--studio-accent2);color:var(--studio-accent2);}
.ms-stage-body{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:12px;}
.ms-stage-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--studio-muted);gap:10px;padding:40px 20px;text-align:center;}
.ms-stage-empty .icon{font-size:52px;opacity:.25;}
.ms-stage-empty p{font-size:12px;line-height:1.7;max-width:240px;opacity:.7;}
/* ── Sep model cards ── */
.sep-model-opt{display:block;padding:8px 10px;border:1.5px solid var(--studio-border2);border-radius:9px;cursor:pointer;transition:.15s;background:transparent;color:var(--studio-text);}
.sep-model-opt:hover{border-color:#a855f7;background:rgba(168,85,247,.06);}
.sep-model-opt.sep-model-active{border-color:#a855f7;background:rgba(168,85,247,.12);}
/* ── Resize handle ── */
.ms-resize{width:3px;flex-shrink:0;background:var(--studio-border);cursor:col-resize;transition:.2s;}
.ms-resize:hover,.ms-resize.dragging{background:var(--studio-accent2);box-shadow:0 0 6px var(--studio-glow);}
/* ── Chat panel ── */
.ms-chat{width:340px;flex-shrink:0;display:flex;flex-direction:column;border-left:1px solid var(--studio-border);background:var(--studio-panel);min-height:0;overflow:hidden;}
.ms-chat-hdr{display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--studio-border);background:var(--studio-panel);flex-shrink:0;gap:6px;}
.ms-chat-hdr-label{font-size:9.5px;font-weight:700;color:var(--studio-accent2);text-transform:uppercase;letter-spacing:.1em;flex-shrink:0;}
.ms-model-sel{flex:1;background:var(--studio-panel2);border:1px solid var(--studio-border2);border-radius:6px;padding:4px 7px;font-size:11px;color:var(--studio-text);font-family:inherit;min-width:0;}
.ms-chat-clear{background:none;border:none;cursor:pointer;font-size:15px;color:var(--studio-muted);padding:2px 4px;transition:.15s;}
.ms-chat-clear:hover{color:var(--studio-red);}
.ms-msgs{flex:1;overflow-y:auto;padding:12px 10px;display:flex;flex-direction:column;gap:10px;}
.ms-msg{display:flex;flex-direction:column;}
.ms-msg.user{align-items:flex-end;}
.ms-msg.assistant{align-items:flex-start;}
.ms-msg.system{align-items:center;}
.ms-bubble{
padding:8px 12px;border-radius:12px;font-size:12px;
line-height:1.6;max-width:92%;word-break:break-word;
}
.ms-msg.user .ms-bubble{
background:linear-gradient(135deg,#4c1d95,#5b21b6);
color:#f3e8ff;border-radius:12px 12px 3px 12px;
box-shadow:0 2px 8px rgba(91,33,182,.3);
}
.ms-msg.assistant .ms-bubble{
background:var(--studio-panel2);
border:1px solid var(--studio-border2);
border-radius:12px 12px 12px 3px;
}
.ms-msg.system .ms-bubble{background:transparent;color:var(--studio-muted);font-size:11px;}
.ms-bubble pre{background:var(--studio-bg);border:1px solid var(--studio-border2);border-radius:8px;padding:10px;overflow-x:auto;font-size:11px;margin:6px 0;}
.ms-bubble code{font-size:11px;}
.ms-bubble p{margin:3px 0;}
.ms-bubble h3{font-size:13px;margin:6px 0 3px;}
.ms-bubble ul,.ms-bubble ol{padding-left:16px;margin:3px 0;}
.ms-thinking{padding:5px 10px;border-radius:7px;font-size:10px;color:var(--studio-muted);background:var(--studio-surface);border:1px solid var(--studio-border);font-style:italic;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;cursor:pointer;margin-bottom:4px;}
.ms-thinking.exp{display:block;-webkit-line-clamp:unset;max-height:200px;overflow-y:auto;}
.ms-typing{display:flex;gap:4px;align-items:center;padding:6px 10px;}
.ms-dot{width:5px;height:5px;background:var(--studio-muted);border-radius:50%;animation:msDot 1.2s ease-in-out infinite;}
.ms-dot:nth-child(2){animation-delay:.2s;}.ms-dot:nth-child(3){animation-delay:.4s;}
@keyframes msDot{0%,80%,100%{transform:scale(.5);opacity:.35;}40%{transform:scale(1);opacity:1;}}
.ms-stage-link{font-size:10px;color:var(--studio-accent2);cursor:pointer;text-decoration:underline;margin-top:2px;display:inline-block;}
/* ── Input ── */
.ms-input-bar{flex-shrink:0;padding:10px;border-top:1px solid var(--studio-border);display:flex;gap:7px;align-items:flex-end;background:var(--studio-panel);}
.ms-textarea{
flex:1;background:var(--studio-panel2);border:1px solid var(--studio-border2);
border-radius:10px;padding:8px 12px;font-size:12px;color:var(--studio-text);
font-family:inherit;resize:none;min-height:36px;max-height:120px;outline:none;transition:.2s;
}
.ms-textarea:focus{border-color:var(--studio-accent2);box-shadow:0 0 0 2px var(--studio-glow2);}
.ms-send{
background:linear-gradient(135deg,var(--studio-accent),#7c3aed);
border:none;border-radius:10px;padding:8px 15px;font-size:12px;color:#fff;
cursor:pointer;font-family:inherit;font-weight:700;transition:.2s;flex-shrink:0;
box-shadow:0 2px 8px var(--studio-glow);
}
.ms-send:hover:not(:disabled){background:linear-gradient(135deg,#a855f7,#8b5cf6);box-shadow:0 4px 14px var(--studio-glow);}
.ms-send:disabled{opacity:.45;cursor:not-allowed;}
/* ── MIDI widgets (stage) ── */
.music-widget{border:1px solid var(--studio-border2);border-radius:12px;background:var(--studio-panel);overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,.3);}
.music-widget-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--studio-panel2);border-bottom:1px solid var(--studio-border);}
.music-widget-label{font-size:12px;font-weight:700;color:var(--studio-text);letter-spacing:.02em;}
.music-render-btn{background:#1e3a5f;border:1px solid #3b82f6;color:#93c5fd;border-radius:6px;padding:3px 9px;font-size:11px;cursor:pointer;font-family:inherit;transition:.2s;}
.music-render-btn:hover:not(:disabled){background:#1d4ed8;color:#fff;box-shadow:0 0 8px rgba(59,130,246,.4);}
.music-render-btn:disabled{opacity:.5;cursor:not-allowed;}
.music-widget-body{padding:10px 12px;}
.music-widget-body.collapsed{display:none;}
.music-output{margin-top:6px;}
.music-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;}
.music-dl-btn{display:inline-flex;align-items:center;gap:4px;background:var(--studio-panel);border:1px solid var(--studio-border2);border-radius:7px;padding:4px 10px;font-size:11px;color:var(--studio-muted);cursor:pointer;text-decoration:none;font-family:inherit;transition:.2s;}
.music-dl-btn:hover{border-color:var(--studio-accent2);color:var(--studio-accent2);}
.music-dl-btn.btn-green{background:#052e16;border-color:#22c55e;color:#86efac;}
.music-dl-btn.btn-purple{background:#1e0442;border-color:var(--studio-accent2);color:#d8b4fe;}
.music-dl-btn.btn-amber{background:#1c0700;border-color:#f59e0b;color:#fcd34d;}
.music-page-btn{background:transparent;border:1px solid var(--studio-border2);border-radius:5px;padding:2px 8px;font-size:11px;cursor:pointer;color:var(--studio-muted);font-family:inherit;transition:.2s;}
.music-page-btn:hover{border-color:var(--studio-accent2);color:var(--studio-accent2);}
.music-page-nav{display:flex;align-items:center;gap:6px;margin-bottom:6px;font-size:11px;color:var(--studio-muted);}
.midi-router-panel{display:flex;flex-wrap:wrap;gap:5px;align-items:center;margin-top:6px;}
.midi-router-sel{background:var(--studio-panel2);border:1px solid var(--studio-border2);border-radius:6px;padding:2px 6px;font-size:11px;color:var(--studio-text);}
/* ── MIDI editor (piano roll) ── */
.ms-editor{display:none;flex-direction:column;flex:1;min-height:0;overflow:hidden;}
.ms-editor.visible{display:flex;}
.ms-editor-bar{display:flex;align-items:center;gap:8px;padding:7px 14px;background:#191a2c;border-bottom:1px solid rgba(255,255,255,.06);flex-shrink:0;}
.ms-editor-back{background:#1e1e30;border:1px solid rgba(255,255,255,.1);color:#c8cadc;border-radius:7px;padding:4px 12px;font-size:12px;cursor:pointer;font-family:inherit;transition:.2s;}
.ms-editor-back:hover{background:#2a2a3e;border-color:var(--studio-accent2);}
.ms-editor-title{font-size:12px;font-weight:700;color:#c8cadc;flex:1;letter-spacing:.03em;}
#me-body{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden;color:#c8cadc;font-size:13px;user-select:none;}
#toolbar{display:flex;align-items:center;gap:3px;padding:5px 10px;background:#191a2c;border-bottom:1px solid rgba(255,255,255,.06);flex-shrink:0;flex-wrap:wrap;min-height:38px;}
.tb-sep{width:1px;height:20px;background:rgba(255,255,255,.08);margin:0 5px;}
.tb-btn{background:#1e1e30;border:1px solid rgba(255,255,255,.1);color:#9a9cb8;border-radius:6px;padding:3px 9px;cursor:pointer;font-size:11px;white-space:nowrap;transition:.15s;}
.tb-btn:hover{background:#282840;border-color:rgba(168,85,247,.4);color:#c8cadc;}
.tb-btn.active{background:linear-gradient(135deg,#4c1d95,#5b21b6);color:#fff;border-color:var(--studio-accent);box-shadow:0 0 8px var(--studio-glow);}
.tb-lbl{color:#4a4d6a;font-size:11px;letter-spacing:.04em;}
select.tb-sel{background:#1e1e30;border:1px solid rgba(255,255,255,.1);color:#c8cadc;border-radius:6px;padding:2px 5px;font-size:11px;}
input.tb-num{background:#1e1e30;border:1px solid rgba(255,255,255,.1);color:#c8cadc;border-radius:6px;padding:2px 7px;width:58px;font-size:11px;text-align:center;}
#midi-out-sel{background:#1e1e30;border:1px solid rgba(255,255,255,.1);color:#c8cadc;border-radius:6px;padding:2px 5px;font-size:11px;max-width:160px;}
#me-main{display:flex;flex:1;overflow:hidden;}
#track-panel{width:168px;flex-shrink:0;background:#191a2c;border-right:1px solid rgba(255,255,255,.06);display:flex;flex-direction:column;overflow:hidden;}
#tp-head{padding:5px 10px;color:#4a4d6a;font-size:10px;border-bottom:1px solid rgba(255,255,255,.06);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;letter-spacing:.06em;text-transform:uppercase;}
.tp-add-btn{background:none;border:1px solid rgba(255,255,255,.15);border-radius:4px;color:#6a6888;cursor:pointer;font-size:13px;line-height:1;padding:0 5px;}
#track-list{flex:1;overflow-y:auto;}
.ti{padding:5px 8px 4px;border-bottom:1px solid rgba(255,255,255,.04);cursor:pointer;transition:.15s;}
.ti:hover{background:rgba(255,255,255,.03);}
.ti.sel{background:rgba(168,85,247,.1);border-left:2px solid var(--studio-accent2);}
.ti-top{display:flex;align-items:center;gap:5px;}
.ti-dot{width:10px;height:10px;border-radius:3px;flex-shrink:0;cursor:pointer;}
.ti-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;color:#c8cadc;}
.ti-ch{color:#4a4d6a;font-size:10px;flex-shrink:0;}
.ti-btns{display:flex;gap:2px;margin-top:3px;}
.ti-btn{background:#272840;border:1px solid rgba(255,255,255,.1);color:#6e7096;border-radius:4px;padding:1px 6px;cursor:pointer;font-size:10px;transition:.12s;}
.ti-btn:hover{background:#252538;color:#c8cadc;}
.ti-btn.m-on{background:#7f1d1d;color:#fca5a5;border-color:#ef4444;}
.ti-btn.s-on{background:#14532d;color:#86efac;border-color:#22c55e;}
.ti-prog{font-size:9px;color:#383b52;margin-top:2px;}
#roll-wrap{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;}
#ruler-row{display:flex;flex-shrink:0;border-bottom:1px solid rgba(255,255,255,.06);}
#corner{width:56px;flex-shrink:0;background:#161727;border-right:1px solid rgba(255,255,255,.06);height:24px;display:flex;align-items:center;justify-content:center;}
#corner span{color:#2e3050;font-size:9px;}
#ruler-canvas{flex:1;display:block;background:#161727;height:24px;cursor:pointer;}
#roll-row{flex:1;display:flex;overflow:hidden;min-height:0;}
#keys-canvas{display:block;flex-shrink:0;width:56px;background:#161727;border-right:1px solid rgba(255,255,255,.06);cursor:pointer;}
#roll-canvas{display:block;flex:1;min-width:0;cursor:default;}
#vel-row{height:72px;flex-shrink:0;display:flex;border-top:1px solid rgba(255,255,255,.06);}
#vel-label{width:56px;flex-shrink:0;background:#161727;border-right:1px solid rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;color:#2e3050;font-size:9px;}
#vel-canvas{display:block;flex:1;min-width:0;cursor:ns-resize;background:#191a2c;}
#master-channel{display:flex;align-items:center;gap:6px;padding:4px 10px;background:#141425;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0;flex-wrap:wrap;}
.mc-label{font-size:9px;font-weight:700;letter-spacing:.1em;color:#6a5fa8;text-transform:uppercase;white-space:nowrap;}
.mc-lbl{font-size:10px;color:#8888a8;white-space:nowrap;display:inline-flex;align-items:center;gap:3px;cursor:pointer;}
.mc-sep{width:1px;height:16px;background:rgba(255,255,255,.1);flex-shrink:0;}
.mc-sl{width:72px;accent-color:#a78bfa;cursor:pointer;vertical-align:middle;}
.mc-val{font-size:10px;color:#6a6888;min-width:30px;}
#transport{display:flex;align-items:center;gap:7px;padding:5px 12px;background:#191a2c;border-top:1px solid rgba(255,255,255,.06);flex-shrink:0;}
.tr-btn{background:#1e1e30;border:1px solid rgba(255,255,255,.1);color:#c8cadc;border-radius:6px;padding:3px 12px;cursor:pointer;font-size:14px;transition:.15s;}
.tr-btn:hover{background:#282840;border-color:rgba(168,85,247,.4);}
#pos-disp{font-family:'SF Mono',Monaco,monospace;color:#86efac;font-size:12px;min-width:90px;letter-spacing:.05em;}
#status-txt{font-size:11px;color:#4a4d6a;}
/* ── Drop overlay ── */
.ms-drop{position:fixed;inset:0;background:rgba(147,51,234,.12);border:2px dashed var(--studio-accent2);border-radius:16px;z-index:100;display:none;align-items:center;justify-content:center;font-size:18px;color:var(--studio-accent2);font-weight:700;pointer-events:none;backdrop-filter:blur(2px);}
.ms-drop.on{display:flex;}
/* ── Scrollbars ── */
*::-webkit-scrollbar{width:4px;height:4px;}
*::-webkit-scrollbar-track{background:transparent;}
*::-webkit-scrollbar-thumb{background:var(--studio-border2);border-radius:4px;}
*::-webkit-scrollbar-thumb:hover{background:var(--studio-accent2);}
/* ── Drum box ── */
#stage-drums{display:none;flex-direction:column;flex:1;min-height:0;overflow:hidden;}
#drum-grid{flex:1;overflow:auto;padding:12px 14px;display:flex;flex-direction:column;gap:6px;background:var(--studio-bg);}
.drum-row{display:flex;align-items:center;gap:6px;}
.drum-label{width:88px;flex-shrink:0;font-size:11px;color:var(--studio-muted);text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.drum-cells{display:flex;gap:3px;flex:1;min-width:0;}
.drum-cell{flex:1;min-width:14px;height:26px;border-radius:5px;background:var(--studio-surface);border:1px solid var(--studio-border);cursor:pointer;transition:.1s;padding:0;}
.drum-cell:hover{border-color:var(--studio-accent2);background:var(--studio-glow2);}
.drum-cell.on{background:linear-gradient(135deg,var(--studio-accent),#7c3aed);border-color:var(--studio-accent);box-shadow:0 0 6px var(--studio-glow);}
.drum-cell.cur{outline:2px solid #fbbf24;outline-offset:-1px;}
.drum-row.beat .drum-cell{background:var(--studio-panel);}
.drum-row.beat .drum-cell.on{background:linear-gradient(135deg,var(--studio-accent),#7c3aed);}
.drum-steps-sel{background:var(--studio-panel2);border:1px solid var(--studio-border2);border-radius:6px;padding:2px 6px;font-size:11px;color:var(--studio-text);}
/* ── Looper card ── */
.drum-loop-panel{border-top:1px solid var(--studio-border);background:var(--studio-bg);padding:12px 14px 14px;flex-shrink:0;}
.loop-card{border:1px solid var(--studio-border2);border-radius:14px;background:var(--studio-panel);overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.4);}
.loop-card-hdr{display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--studio-border);background:var(--studio-panel2);}
.loop-card-title{font-size:13px;font-weight:700;color:var(--studio-text);letter-spacing:.03em;}
.loop-card-status{margin-left:auto;font-size:11px;color:var(--studio-muted);white-space:nowrap;}
.loop-meter-bar{height:4px;background:var(--studio-surface);position:relative;flex-shrink:0;}
.loop-meter-fill{height:100%;width:0%;background:linear-gradient(90deg,#22c55e,#eab308,#ef4444);transition:width .05s linear;}
.loop-transport{display:flex;align-items:center;gap:10px;padding:12px 14px 8px;}
.loop-btn{
display:flex;flex-direction:column;align-items:center;gap:4px;
border:1px solid var(--studio-border2);border-radius:12px;
padding:10px 18px;font-size:11px;color:var(--studio-muted);cursor:pointer;
background:var(--studio-panel2);font-family:inherit;transition:.2s;min-width:62px;
letter-spacing:.03em;
}
.loop-btn .lb-icon{font-size:22px;line-height:1;}
.loop-btn:hover{border-color:var(--studio-accent2);color:var(--studio-text);background:var(--studio-glow2);}
.loop-btn.rec{border-color:rgba(239,68,68,.5);color:#fca5a5;}
.loop-btn.rec:hover,.loop-btn.rec.active{background:#7f1d1d;border-color:var(--studio-red);color:#fff;box-shadow:0 0 12px rgba(239,68,68,.4);animation:lpPulse 1s ease-in-out infinite;}
.loop-btn.play.active{background:rgba(34,197,94,.12);border-color:var(--studio-green);color:#86efac;box-shadow:0 0 10px rgba(34,197,94,.25);}
.loop-sec{display:flex;align-items:center;gap:7px;padding:0 14px 12px;flex-wrap:wrap;}
.loop-sec select,.loop-sec button{font-size:11px;font-family:inherit;padding:4px 10px;border:1px solid var(--studio-border2);border-radius:7px;background:var(--studio-panel2);color:var(--studio-muted);cursor:pointer;transition:.2s;}
.loop-sec select:hover,.loop-sec button:hover{border-color:var(--studio-accent2);color:var(--studio-text);}
.loop-sec .loop-del-all{border-color:rgba(239,68,68,.4);color:#fca5a5;}
.loop-sec .loop-del-all:hover{background:#7f1d1d;border-color:var(--studio-red);color:#fff;}
.loop-layers{display:flex;flex-direction:column;gap:0;border-top:1px solid var(--studio-border);}
.loop-empty{color:var(--studio-muted);font-size:11px;line-height:1.7;padding:12px 14px;text-align:center;}
.loop-layer{display:flex;align-items:center;gap:9px;padding:8px 14px;border-bottom:1px solid var(--studio-border);}
.loop-layer:last-child{border-bottom:none;}
.loop-layer .ll-num{width:22px;height:22px;border-radius:50%;background:rgba(147,51,234,.18);border:1px solid rgba(168,85,247,.5);color:#c084fc;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.loop-layer .ll-mute{background:none;border:1px solid var(--studio-border2);border-radius:6px;padding:3px 7px;font-size:12px;cursor:pointer;color:var(--studio-muted);font-family:inherit;flex-shrink:0;transition:.15s;}
.loop-layer .ll-mute.muted{background:#7f1d1d;border-color:var(--studio-red);color:#fca5a5;}
.loop-layer input[type=range]{flex:1;min-width:0;accent-color:var(--studio-accent2);}
.loop-layer .ll-dur{font-size:10px;color:var(--studio-muted);width:36px;text-align:right;flex-shrink:0;}
.loop-layer .ll-del{background:none;border:none;cursor:pointer;color:var(--studio-muted);font-size:15px;padding:2px 5px;flex-shrink:0;transition:.15s;border-radius:4px;}
.loop-layer .ll-del:hover{color:var(--studio-red);background:rgba(239,68,68,.1);}
#loop-rec-btn.recording{animation:lpPulse 1s ease-in-out infinite;}
@keyframes lpPulse{0%,100%{opacity:1;}50%{opacity:.5;}}
@keyframes recPulse{0%,100%{opacity:1;}50%{opacity:.55;}}
/* ── Audio track area ── */
#wave-area{display:flex;flex-direction:column;flex-shrink:0;border-bottom:1px solid rgba(255,255,255,.07);}
#wave-lanes{display:flex;flex-direction:column;}
.wave-lane{display:flex;height:64px;border-bottom:1px solid rgba(255,255,255,.06);flex-shrink:0;}
.wave-lane-ctrl{width:56px;flex-shrink:0;background:#161727;border-right:1px solid rgba(255,255,255,.07);display:flex;align-items:center;justify-content:center;}
.wl-color-strip{width:3px;height:38px;border-radius:2px;}
.wave-lane-cv-wrap{flex:1;position:relative;overflow:hidden;background:#191a2c;}
.wave-lane-cv{position:absolute;inset:0;width:100%;height:100%;}
.wave-lane-hdr{position:absolute;top:0;left:0;right:0;height:23px;display:flex;align-items:center;padding:0 7px;gap:5px;background:linear-gradient(to bottom,rgba(10,10,30,.65) 0%,transparent 100%);pointer-events:none;}
.wl-name{flex:1;font-size:10px;color:rgba(205,202,228,.75);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:text;pointer-events:all;}
.wl-name:hover{color:#dddaf5;}
.wl-hdr-btns{display:flex;align-items:center;gap:3px;pointer-events:all;}
.wl-hbtn{background:rgba(255,255,255,.09);border:1px solid rgba(255,255,255,.15);border-radius:3px;cursor:pointer;font-size:9px;color:#9496b8;padding:1px 5px;line-height:1.4;}
.wl-hbtn:hover{background:rgba(255,255,255,.18);color:#d8daf0;}
.wl-hbtn.muted{color:#ef4444;border-color:rgba(239,68,68,.5);}
.wl-hbtn.rec{color:#f87171;border-color:rgba(248,113,113,.5);}
.wl-hbtn.rec.armed{color:#fca5a5;border-color:#ef4444;background:rgba(239,68,68,.1);}
.wl-hbtn.rec.active{background:rgba(127,29,29,.8);color:#fecaca;border-color:#ef4444;animation:recPulse .9s ease-in-out infinite;}
.wl-hbtn.del:hover{background:rgba(239,68,68,.2);border-color:#ef4444;color:#fca5a5;}
.wl-vol-sm{width:46px;accent-color:#a78bfa;cursor:pointer;vertical-align:middle;}
#wave-add-bar{display:flex;align-items:center;gap:6px;padding:4px 8px;background:rgba(255,255,255,.025);border-top:1px solid rgba(255,255,255,.06);}
#wave-add-bar::before{content:'오디오 트랙';font-size:9px;color:#5a5a78;margin-right:2px;letter-spacing:.04em;}
.wave-add-btn{background:rgba(255,255,255,.05);border:1px dashed rgba(255,255,255,.18);border-radius:5px;color:#8888a8;font-size:10px;cursor:pointer;padding:3px 10px;}
.wave-add-btn:hover{background:rgba(167,139,250,.12);color:#c0b8f0;border-style:solid;border-color:rgba(167,139,250,.4);}
/* ── Drum box specific ── */
.drum-box-sb-btn{
background:rgba(147,51,234,.15);border:1px solid rgba(168,85,247,.4);
color:#c084fc;border-radius:9px;padding:7px 10px;font-size:12px;font-weight:600;
cursor:pointer;font-family:inherit;text-align:left;transition:.2s;margin-top:4px;
display:block;width:100%;letter-spacing:.02em;
}
.drum-box-sb-btn:hover{background:rgba(147,51,234,.3);border-color:var(--studio-accent2);color:#e9d5ff;box-shadow:0 0 10px var(--studio-glow2);}
.drum-back-btn{
background:rgba(147,51,234,.15);border:1px solid rgba(168,85,247,.4);
color:#c084fc;border-radius:7px;padding:5px 14px;font-size:12px;font-weight:600;
cursor:pointer;font-family:inherit;transition:.2s;
}
.drum-back-btn:hover{background:rgba(147,51,234,.3);border-color:var(--studio-accent2);color:#e9d5ff;}
/* ── Folder popup ── */
#fp-overlay{display:none;position:fixed;inset:0;z-index:950;background:rgba(0,0,0,.6);backdrop-filter:blur(3px);}
#fp-overlay.open{display:block;}
#fp-sheet{
position:absolute;bottom:0;left:0;right:0;
max-height:72vh;background:var(--studio-panel);
border-radius:16px 16px 0 0;
border-top:1px solid var(--studio-border2);
display:flex;flex-direction:column;overflow:hidden;
box-shadow:0 -8px 40px rgba(0,0,0,.5);
transform:translateY(100%);transition:transform .25s ease;
}
#fp-overlay.open #fp-sheet{transform:translateY(0);}
@media(min-width:1025px){
#fp-sheet{
position:absolute;top:50%;left:50%;bottom:auto;right:auto;
width:400px;max-height:65vh;
border-radius:14px;border:1px solid var(--studio-border2);
box-shadow:0 20px 60px rgba(0,0,0,.6);
transform:translate(-50%,-46%);transition:none;
}
#fp-overlay.open #fp-sheet{transform:translate(-50%,-50%);}
}
.fp-hdr{display:flex;align-items:center;padding:13px 16px 10px;flex-shrink:0;border-bottom:1px solid var(--studio-border);gap:6px;}
.fp-title{font-size:13px;font-weight:700;color:var(--studio-text);flex:1;}
.fp-icon-btn{background:none;border:none;color:var(--studio-muted);cursor:pointer;font-size:14px;padding:3px 6px;border-radius:6px;transition:.15s;font-family:inherit;}
.fp-icon-btn:hover{color:var(--studio-accent2);background:var(--studio-glow2);}
.fp-tabs{display:flex;gap:4px;padding:8px 12px 6px;flex-shrink:0;}
.fp-tab{flex:1;padding:6px;font-size:11px;font-weight:700;border:1px solid var(--studio-border2);border-radius:8px;background:transparent;color:var(--studio-muted);cursor:pointer;font-family:inherit;transition:.2s;}
.fp-tab.active{background:var(--studio-accent);border-color:var(--studio-accent);color:#fff;box-shadow:0 0 10px var(--studio-glow);}
.fp-body{flex:1;overflow-y:auto;padding:4px 8px 14px;min-height:0;}
.fp-item{display:flex;align-items:center;gap:6px;padding:7px 8px;border-radius:8px;transition:.15s;}
.fp-item:hover{background:var(--studio-glow2);}
.fp-item-icon{font-size:15px;flex-shrink:0;}
.fp-item-name{flex:1;font-size:12px;color:var(--studio-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;}
.fp-item-del{background:none;border:none;color:var(--studio-muted);cursor:pointer;font-size:13px;padding:2px 5px;border-radius:4px;flex-shrink:0;transition:.15s;line-height:1;}
.fp-item-del:hover{color:var(--studio-red);}
.fp-empty{font-size:12px;color:var(--studio-muted);padding:24px 12px;text-align:center;}
/* ── Mobile ── */
@media(max-width:1024px){
.ms-hdr{padding:0 10px;gap:6px;}
.ms-hdr h1{font-size:13px;}
/* Sidebar → left overlay */
.ms-sb{
position:fixed;top:0;left:0;bottom:52px;z-index:500;
width:82vw;max-width:300px;
transform:translateX(-100%);transition:transform .25s ease;
box-shadow:4px 0 28px rgba(0,0,0,.5);
}
.ms-sb.mob-open{transform:translateX(0);}
/* Chat → right overlay */
.ms-chat{
position:fixed;top:0;right:0;bottom:52px;z-index:500;
width:92vw;max-width:360px;
transform:translateX(100%);transition:transform .25s ease;
box-shadow:-4px 0 28px rgba(0,0,0,.5);
}
.ms-chat.mob-open{transform:translateX(0);}
#mob-backdrop{display:none;position:fixed;inset:0 0 52px 0;background:rgba(0,0,0,.55);z-index:499;}
#mob-backdrop.show{display:block;}
#mob-tabbar{display:flex !important;position:fixed;bottom:0;left:0;right:0;z-index:600;background:var(--studio-panel);border-top:1px solid var(--studio-border);height:52px;}
.ms-layout{padding-bottom:52px;}
/* Master channel: wrap on small screens */
#master-channel{flex-wrap:wrap;gap:4px;}
input,textarea,select{font-size:16px !important;}
}
@media(min-width:1025px){
#mob-backdrop{display:none !important;}
#mob-tabbar{display:none !important;}
}
</style>
<!-- AudioMass built-in -->
<link rel="stylesheet" href="/audiomass/main.css">
<style>
/* AudioMass icon font override — must win over body font-family inheritance */
#audiomass-stage .pk_tb .pk_btn,
#audiomass-stage [class*="icon-"] {
font-family: 'icomoon' !important;
color: #eef2ff !important;
}
#audiomass-stage .pk_tb .pk_btn:hover {
color: #5af2ff !important;
}
#audiomass-stage .pk_tb .pk_btn.pk_act {
color: #5af2ff !important;
}
#audiomass-stage .pk_hdr,
#audiomass-stage .pk_hdr .pk_btn button {
color: #eef2ff !important;
}
</style>
</head>
<body>
<div class="ms-drop" id="drop-overlay">🎵 MIDI 파일을 드롭하세요</div>
<div id="mob-backdrop" onclick="mobCloseAll()"></div>
<div id="mob-tabbar" style="display:none">
<button class="mob-tab" id="mobtab-files" onclick="mobToggle('files')"><span style="font-size:18px">📂</span>파일</button>
<button class="mob-tab active" id="mobtab-stage" onclick="mobCloseAll()"><span style="font-size:18px">🎵</span>스테이지</button>
<button class="mob-tab" id="mobtab-ai" onclick="mobToggle('ai')"><span style="font-size:18px">🤖</span>AI</button>
</div>
<header class="ms-hdr">
<a href="/" class="ms-hdr-btn">← 홈</a>
<h1>🎵 음악 스튜디오</h1>
<span class="skill-badge" id="skill-badge">스킬 로딩 중...</span>
<div class="ms-hdr-spacer"></div>
<button class="ms-hdr-btn" onclick="toggleTheme()">🌙 테마</button>
</header>
<div class="ms-layout">
<!-- ── Left sidebar ── -->
<aside class="ms-sb">
<div class="ms-sb-sec">빠른 요청</div>
<div class="ms-quick">
<button class="ms-qbtn" onclick="sendQuick('밝고 경쾌한 피아노 소곡을 C장조로 작곡해서 MIDI로 생성해주세요.')">🎹 피아노 소곡 작곡</button>
<button class="ms-qbtn" onclick="sendQuick('재즈 코드 진행 Am7-D7-Gmaj7-Cmaj7를 분석하고 MIDI로 생성해주세요.')">🎸 재즈 코드 진행</button>
<button class="ms-qbtn" onclick="sendQuick('밝고 신나는 재즈 잼을 작곡해주세요. 피아노, 베이스, 드럼으로 구성해주세요.')">🎷 재즈 잼 작곡</button>
<button class="ms-qbtn" onclick="sendQuick('C장조 음계, 코드, 기본 화성법을 간단히 설명해주세요.')">📚 이론 설명</button>
<button class="ms-qbtn" onclick="sendQuick('업로드한 악보나 이미지를 분석해주세요.')">📄 악보 분석</button>
<button class="drum-box-sb-btn" onclick="showAudioMass(true)">✂️ 오디오 편집</button>
<button class="drum-box-sb-btn" onclick="openWavacity()">🎚 Wavacity</button>
<button class="drum-box-sb-btn" onclick="showYtPanel(true)">🎬 YouTube</button>
<button class="drum-box-sb-btn" onclick="showSepPanel(true)">🎙 보컬 분리</button>
<button class="drum-box-sb-btn" onclick="showStageDrums()">🥁 드럼박스</button>
</div>
<div class="ms-divider"></div>
<div class="ms-file-hdr">
<span>MIDI 파일</span>
<button onclick="loadMidiFiles()" title="새로고침">↺</button>
</div>
<div class="ms-files" id="midi-file-list">
<div style="font-size:11px;color:var(--muted);padding:4px 6px">로딩 중...</div>
</div>
<label class="ms-upload">
⬆ MIDI 업로드
<input type="file" accept=".mid,.midi" style="display:none" onchange="uploadMidiFile(this.files[0]);this.value=''">
</label>
<div class="ms-divider"></div>
<div class="ms-file-hdr">
<span>MIDI 다운로드</span>
<button onclick="openFolderPopup('midi')" title="저장된 파일 보기" style="font-size:13px;padding:1px 6px;border:1px solid var(--studio-border2);border-radius:6px;background:var(--studio-panel2);color:var(--studio-muted);cursor:pointer;transition:.15s;" onmouseover="this.style.borderColor='var(--studio-accent2)';this.style.color='var(--studio-accent2)'" onmouseout="this.style.borderColor='var(--studio-border2)';this.style.color='var(--studio-muted)'">📂</button>
</div>
<!-- MIDI download site picker -->
<div style="padding:4px 6px 2px;display:flex;gap:3px;">
<button id="mdl-site-midifind" onclick="mdlSetSite('midifind')" style="font-size:10px;padding:2px 6px;border:1px solid #a855f7;border-radius:4px;background:#a855f7;color:#fff;cursor:pointer;font-family:inherit;">midifind</button>
<button id="mdl-site-bitmidi" onclick="mdlSetSite('bitmidi')" style="font-size:10px;padding:2px 6px;border:1px solid var(--line);border-radius:4px;background:var(--panel);color:var(--muted);cursor:pointer;font-family:inherit;">bitmidi</button>
</div>
<div style="padding:2px 6px 4px;display:flex;gap:4px;">
<input id="mdl-search-input" type="text" placeholder="곡명/아티스트 검색..." onkeydown="if(event.key==='Enter')mdlSearch()"
style="flex:1;font-size:11px;padding:3px 7px;border:1px solid var(--line);border-radius:5px;background:var(--bg);color:var(--text);font-family:inherit;outline:none;">
<button onclick="mdlSearch()" style="font-size:11px;padding:3px 8px;border:1px solid var(--line);border-radius:5px;background:var(--panel);color:var(--muted);cursor:pointer;">🔍</button>
</div>
<div class="ms-files" id="mdl-search-results" style="max-height:150px;overflow-y:auto;">
<div style="font-size:11px;color:var(--muted);padding:4px 8px">곡명 또는 아티스트로 검색</div>
</div>
<div class="ms-divider"></div>
<div class="ms-file-hdr">
<span>Guitar Pro</span>
<button onclick="openFolderPopup('gp')" title="저장된 파일 보기" style="font-size:13px;padding:1px 6px;border:1px solid var(--studio-border2);border-radius:6px;background:var(--studio-panel2);color:var(--studio-muted);cursor:pointer;transition:.15s;" onmouseover="this.style.borderColor='var(--studio-accent2)';this.style.color='var(--studio-accent2)'" onmouseout="this.style.borderColor='var(--studio-border2)';this.style.color='var(--studio-muted)'">📂</button>
</div>
<!-- GP 검색 -->
<div style="padding:4px 6px 2px;display:flex;gap:3px;">
<button id="gp-site-gptabs" onclick="gpSetSite('guitarprotabs')" style="font-size:10px;padding:2px 6px;border:1px solid #a855f7;border-radius:4px;background:#a855f7;color:#fff;cursor:pointer;font-family:inherit;">gptabs</button>
<button id="gp-site-gprotab" onclick="gpSetSite('gprotab')" style="font-size:10px;padding:2px 6px;border:1px solid var(--line);border-radius:4px;background:var(--panel);color:var(--muted);cursor:pointer;font-family:inherit;">gprotab</button>
</div>
<div style="padding:2px 6px 4px;display:flex;gap:4px;">
<input id="gp-search-input" type="text" placeholder="검색..." onkeydown="if(event.key==='Enter')gpSearch()"
style="flex:1;font-size:11px;padding:3px 7px;border:1px solid var(--line);border-radius:5px;background:var(--bg);color:var(--text);font-family:inherit;outline:none;">
<button onclick="gpSearch()" style="font-size:11px;padding:3px 8px;border:1px solid var(--line);border-radius:5px;background:var(--panel);color:var(--muted);cursor:pointer;">🔍</button>
</div>
<div class="ms-files" id="gp-search-results" style="flex:1;min-height:0;">
<div style="font-size:11px;color:var(--muted);padding:4px 8px">아티스트 또는 곡명으로 검색</div>
</div>
<div class="ms-files" id="gp-file-list" style="max-height:80px;">
</div>
<label class="ms-upload">
⬆ GP 파일 열기
<input type="file" accept=".gp,.gp3,.gp4,.gp5,.gp6,.gpx,.gp7" style="display:none" onchange="openGpFile(this.files[0]);this.value=''">
</label>
</aside>
<!-- ── Center stage ── -->
<section class="ms-stage" id="stage">
<!-- AudioMass panel -->
<div id="audiomass-panel" style="display:none;flex:1;min-height:0;overflow:hidden;flex-direction:column;">
<div style="flex-shrink:0;padding:4px 8px;background:#2c3152;border-bottom:1px solid rgba(90,242,255,.3);display:flex;align-items:center;gap:8px;">
<button onclick="showAudioMass(false)" style="padding:3px 10px;background:rgba(90,242,255,.08);border:1px solid rgba(90,242,255,.3);color:#eef2ff;border-radius:5px;font-size:11px;cursor:pointer;">← 뒤로</button>
<span style="font-size:11px;color:#8898c8;">AudioMass Audio Editor</span>
</div>
<div id="audiomass-stage" style="flex:1;min-height:0;overflow:hidden;"></div>
</div>
<!-- YouTube downloader panel -->
<div id="yt-panel" style="display:none;flex-direction:column;flex:1;min-height:0;overflow:hidden;">
<div class="ms-stage-hdr">
<button class="ms-stage-btn" onclick="ytBack()" style="margin-right:6px">← 뒤로</button>
<span class="ms-stage-title">🎬 YouTube 다운로더</span>
</div>
<div style="flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center;">
<!-- Search / URL tabs -->
<div style="width:100%;max-width:520px;display:flex;flex-direction:column;gap:10px;">
<div style="display:flex;gap:6px;">
<button id="yt-tab-search" onclick="setYtTab('search')"
style="flex:1;padding:7px;background:rgba(168,85,247,.15);border:1.5px solid #a855f7;color:#a855f7;border-radius:8px;font-size:12px;font-weight:700;cursor:pointer;">🔍 검색</button>
<button id="yt-tab-url" onclick="setYtTab('url')"
style="flex:1;padding:7px;background:transparent;border:1.5px solid var(--studio-border2);color:var(--studio-muted);border-radius:8px;font-size:12px;font-weight:700;cursor:pointer;">🔗 URL</button>
</div>
<!-- Search input -->
<div id="yt-search-box" style="display:flex;gap:8px;">
<input id="yt-search-input" type="text" placeholder="제목, 아티스트로 검색…"
style="flex:1;padding:10px 14px;background:var(--studio-panel2);border:1.5px solid var(--studio-border2);border-radius:10px;color:var(--studio-text);font-size:13px;outline:none;"
onfocus="this.style.borderColor='#a855f7'" onblur="this.style.borderColor=''"
onkeydown="if(event.key==='Enter')ytDoSearch()">
<button onclick="ytDoSearch()" id="yt-search-btn"
style="padding:10px 16px;background:#a855f7;color:#fff;border:none;border-radius:10px;font-size:13px;font-weight:700;cursor:pointer;">검색</button>
</div>
<!-- URL input -->
<div id="yt-url-box" style="display:none;display:flex;gap:8px;">
<input id="yt-url-input" type="url" placeholder="YouTube URL을 붙여넣으세요…"
style="flex:1;padding:10px 14px;background:var(--studio-panel2);border:1.5px solid var(--studio-border2);border-radius:10px;color:var(--studio-text);font-size:13px;outline:none;"
onfocus="this.style.borderColor='#a855f7'" onblur="this.style.borderColor=''"
onkeydown="if(event.key==='Enter')ytFetchInfo()">
<button onclick="ytFetchInfo()" id="yt-info-btn"
style="padding:10px 16px;background:#a855f7;color:#fff;border:none;border-radius:10px;font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap;">확인</button>
</div>
</div>
<!-- Search results -->
<div id="yt-search-results" style="display:none;width:100%;max-width:520px;display:flex;flex-direction:column;gap:6px;"></div>
<!-- Video info preview -->
<div id="yt-info" style="display:none;width:100%;max-width:520px;background:var(--studio-panel2);border:1.5px solid var(--studio-border2);border-radius:12px;overflow:hidden;">
<!-- Embedded player -->
<div style="position:relative;width:100%;padding-top:56.25%;background:#000;">
<iframe id="yt-embed" src="" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen
style="position:absolute;top:0;left:0;width:100%;height:100%;border:none;"></iframe>
</div>
<div style="display:flex;gap:12px;padding:10px 12px;">
<div style="flex:1;min-width:0;">
<div id="yt-title" style="font-size:13px;font-weight:700;color:var(--studio-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></div>
<div id="yt-meta" style="font-size:11px;color:var(--studio-muted);margin-top:3px;"></div>
</div>
</div>
<!-- Format selector -->
<div style="padding:0 12px 12px;display:flex;gap:6px;">
<label id="yt-fmt-mp3" onclick="setYtFmt('mp3')"
style="flex:1;display:flex;align-items:center;gap:8px;padding:8px 12px;border:1.5px solid #a855f7;border-radius:9px;cursor:pointer;background:rgba(168,85,247,.1);">
<input type="radio" name="yt-fmt" value="mp3" checked style="accent-color:#a855f7">
<div>
<div style="font-size:12px;font-weight:700;color:var(--studio-text)">MP3 <span style="color:#a855f7;font-size:10px;">추천</span></div>
<div style="font-size:10px;color:var(--studio-muted)">320kbps · 보컬 분리 가능</div>
</div>
</label>
<label id="yt-fmt-best" onclick="setYtFmt('best')"
style="flex:1;display:flex;align-items:center;gap:8px;padding:8px 12px;border:1.5px solid var(--studio-border2);border-radius:9px;cursor:pointer;">
<input type="radio" name="yt-fmt" value="best" style="accent-color:#a855f7">
<div>
<div style="font-size:12px;font-weight:700;color:var(--studio-text)">원본 최고음질</div>
<div style="font-size:10px;color:var(--studio-muted)">M4A/WebM · 무손실</div>
</div>
</label>
</div>
<div style="padding:0 12px 12px;">
<button id="yt-dl-btn" onclick="ytStartDownload()"
style="width:100%;padding:10px;background:linear-gradient(135deg,#7c3aed,#a855f7);color:#fff;border:none;border-radius:9px;font-size:13px;font-weight:700;cursor:pointer;">
⬇ 다운로드 시작
</button>
</div>
</div>
<!-- Progress -->
<div id="yt-progress-wrap" style="display:none;width:100%;max-width:520px;">
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
<span id="yt-status-text" style="font-size:12px;color:var(--studio-muted)">다운로드 중…</span>
<span id="yt-pct-text" style="font-size:12px;font-weight:700;color:#a855f7">0%</span>
</div>
<div style="height:8px;background:var(--studio-border);border-radius:4px;overflow:hidden;">
<div id="yt-progress-bar" style="height:100%;background:linear-gradient(90deg,#7c3aed,#a855f7);border-radius:4px;width:0%;transition:width .4s ease;"></div>
</div>
</div>
<!-- Result -->
<div id="yt-result" style="display:none;width:100%;max-width:520px;background:var(--studio-panel2);border:1.5px solid var(--studio-border2);border-radius:12px;padding:14px;">
<div style="font-size:12px;font-weight:700;color:var(--studio-text);margin-bottom:10px;">✅ 다운로드 완료</div>
<div id="yt-result-body"></div>
</div>
</div>
</div>
<!-- Vocal separator panel -->
<div id="sep-panel" style="display:none;flex-direction:column;flex:1;min-height:0;overflow:hidden;">
<div class="ms-stage-hdr">
<button class="ms-stage-btn" onclick="sepBack()" style="margin-right:6px">← 뒤로</button>
<span class="ms-stage-title">🎙 보컬 / 반주 분리</span>
<div style="display:flex;gap:4px;margin-left:auto;">
<button id="sep-tab-new" class="ms-stage-btn" onclick="setSepTab('new')" style="background:rgba(168,85,247,.15);border-color:#a855f7;color:#a855f7;">+ 새 작업</button>
<button id="sep-tab-files" class="ms-stage-btn" onclick="setSepTab('files')">📂 파일 목록</button>
</div>
</div>
<!-- New job view -->
<div id="sep-view-new" style="flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center;">
<!-- Upload zone -->
<div id="sep-dropzone" style="width:100%;max-width:480px;border:2px dashed var(--studio-border2);border-radius:14px;padding:32px 20px;text-align:center;cursor:pointer;transition:.2s;background:var(--studio-panel2);"
onclick="document.getElementById('sep-file-input').click()"
ondragover="event.preventDefault();this.style.borderColor='#a855f7'"
ondragleave="this.style.borderColor=''"
ondrop="event.preventDefault();this.style.borderColor='';sepHandleFile(event.dataTransfer.files[0])">
<div style="font-size:40px;margin-bottom:8px">🎵</div>
<div style="font-size:13px;color:var(--studio-text);font-weight:600;margin-bottom:4px">MP3 / WAV / FLAC 파일을 드롭</div>
<div style="font-size:11px;color:var(--studio-muted)">또는 클릭하여 파일 선택 · 최대 200MB</div>
<input type="file" id="sep-file-input" accept=".mp3,.wav,.flac,.m4a,.ogg" style="display:none" onchange="sepHandleFile(this.files[0])">
</div>
<!-- Model selector -->
<div style="width:100%;max-width:480px;">
<div style="font-size:11px;color:var(--studio-muted);margin-bottom:6px;font-weight:600;letter-spacing:.04em;">모델 선택</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;" id="sep-model-grid">
<label class="sep-model-opt sep-model-active" data-model="htdemucs" onclick="setSepModel('htdemucs')">
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:12px;font-weight:700">htdemucs</span>
<span style="font-size:9px;padding:1px 5px;background:#a855f7;color:#fff;border-radius:4px;">추천</span>
</div>
<div style="font-size:10px;color:var(--studio-muted);margin-top:2px">Hybrid Transformer · 균형</div>
</label>
<label class="sep-model-opt" data-model="htdemucs_ft" onclick="setSepModel('htdemucs_ft')">
<div style="font-size:12px;font-weight:700">htdemucs_ft</div>
<div style="font-size:10px;color:var(--studio-muted);margin-top:2px">Fine-tuned · 최고 품질 · 느림</div>
</label>
<label class="sep-model-opt" data-model="htdemucs_6s" onclick="setSepModel('htdemucs_6s')">
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:12px;font-weight:700">htdemucs_6s</span>
<span style="font-size:9px;padding:1px 5px;background:#0ea5e9;color:#fff;border-radius:4px;">6분리</span>
</div>
<div style="font-size:10px;color:var(--studio-muted);margin-top:2px">보컬+드럼+베이스+기타+피아노+기타2</div>
</label>
<label class="sep-model-opt" data-model="mdx_extra" onclick="setSepModel('mdx_extra')">
<div style="font-size:12px;font-weight:700">mdx_extra</div>
<div style="font-size:10px;color:var(--studio-muted);margin-top:2px">MDX-Net 고품질</div>
</label>
<label class="sep-model-opt" data-model="hdemucs_mmi" onclick="setSepModel('hdemucs_mmi')">
<div style="font-size:12px;font-weight:700">hdemucs_mmi</div>
<div style="font-size:10px;color:var(--studio-muted);margin-top:2px">구형 Hybrid · 안정적</div>
</label>
<label class="sep-model-opt" data-model="mdx" onclick="setSepModel('mdx')">
<div style="font-size:12px;font-weight:700">mdx</div>
<div style="font-size:10px;color:var(--studio-muted);margin-top:2px">MDX-Net 기본 · 빠름</div>
</label>
</div>
</div>
<!-- Stem mode selector -->
<div style="display:flex;gap:8px;width:100%;max-width:480px;" id="sep-stem-mode-wrap">
<label id="sep-opt-2" onclick="setSepMode('2')" style="flex:1;display:flex;align-items:center;gap:8px;padding:10px 14px;border:2px solid #a855f7;border-radius:10px;cursor:pointer;background:rgba(168,85,247,.1);">
<input type="radio" name="sep-mode" value="2" checked style="accent-color:#a855f7">
<div>
<div style="font-size:12px;font-weight:700;color:var(--studio-text)">2-stem <span style="color:#a855f7">추천</span></div>
<div style="font-size:10px;color:var(--studio-muted)">보컬 + 반주 · 빠름</div>
</div>
</label>
<label id="sep-opt-4" onclick="setSepMode('4')" style="flex:1;display:flex;align-items:center;gap:8px;padding:10px 14px;border:2px solid var(--studio-border2);border-radius:10px;cursor:pointer;">
<input type="radio" name="sep-mode" value="4" style="accent-color:#a855f7">
<div>
<div style="font-size:12px;font-weight:700;color:var(--studio-text)">4-stem</div>
<div style="font-size:10px;color:var(--studio-muted)">보컬+드럼+베이스+기타 · 느림</div>
</div>
</label>
</div>
<!-- Progress area (hidden by default) -->
<div id="sep-progress-wrap" style="display:none;width:100%;max-width:480px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
<span id="sep-status-text" style="font-size:12px;color:var(--studio-muted)">분리 중… (CPU 모드, 수 분 소요)</span>
<span id="sep-pct-text" style="font-size:12px;font-weight:700;color:#a855f7">0%</span>
</div>
<div style="height:8px;background:var(--studio-border);border-radius:4px;overflow:hidden;">
<div id="sep-progress-bar" style="height:100%;background:linear-gradient(90deg,#7c3aed,#a855f7);border-radius:4px;width:0%;transition:width .4s ease;"></div>
</div>
<div style="font-size:10px;color:var(--studio-muted);margin-top:6px;text-align:center">htdemucs 모델 · 보컬/반주 2-stem 분리</div>
</div>
<!-- Results area (hidden by default) -->
<div id="sep-results" style="display:none;width:100%;max-width:480px;display:none;flex-direction:column;gap:10px;">
<div style="font-size:12px;font-weight:700;color:var(--studio-text);margin-bottom:2px">✅ 분리 완료</div>
<div id="sep-stem-list" style="display:flex;flex-direction:column;gap:8px;"></div>
</div>
</div>
<!-- File browser view -->
<div id="sep-view-files" style="display:none;flex:1;overflow-y:auto;padding:16px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
<span style="font-size:11px;font-weight:700;color:var(--studio-muted);letter-spacing:.04em;">저장된 작업</span>
<button class="ms-stage-btn" onclick="loadSepJobs()">↺ 새로고침</button>
</div>
<div id="sep-job-list" style="display:flex;flex-direction:column;gap:10px;">
<div style="text-align:center;color:var(--studio-muted);font-size:12px;padding:40px 0;">불러오는 중…</div>
</div>
</div>
</div>
<!-- Player stage -->
<div id="stage-player" style="display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden;">
<div class="ms-stage-hdr">
<span class="ms-stage-title">🎵 플레이어 스테이지</span>
<button class="ms-stage-btn" onclick="clearStage()">전체 삭제</button>
</div>
<div class="ms-stage-body" id="stage-body">
<div class="ms-stage-empty" id="stage-empty">
<div class="icon">🎵</div>
<p>사이드바에서 파일을 불러오거나<br>AI에게 작곡을 요청하면<br>여기에 MIDI 플레이어가 나타납니다.</p>
</div>
</div>
</div>
<!-- MIDI editor (inline piano roll) -->
<div class="ms-editor" id="stage-editor">
<div class="ms-editor-bar">
<button class="ms-editor-back" onclick="closeEditor()">← 스테이지로</button>
<span class="ms-editor-title" id="editor-title">MIDI 편집기</span>
<button class="ms-editor-back" onclick="doSave()" style="margin-left:auto">💾 저장</button>
</div>
<div id="me-body">
<div id="toolbar">
<button class="tb-btn" onclick="openFileDialog()">📂 열기</button>
<div class="tb-sep"></div>
<button class="tb-btn" onclick="doUndo()" title="Ctrl+Z">↩</button>
<button class="tb-btn" onclick="doRedo()" title="Ctrl+Y">↪</button>
<div class="tb-sep"></div>
<button class="tb-btn" id="t-sel" onclick="setTool('sel')" title="V">▢ 선택</button>
<button class="tb-btn" id="t-draw" onclick="setTool('draw')" title="P">✏ 그리기</button>
<button class="tb-btn" id="t-erase" onclick="setTool('erase')" title="E">✗ 지우개</button>
<div class="tb-sep"></div>
<span class="tb-lbl">양자화</span>
<select class="tb-sel" id="q-sel" onchange="setQ(this.value)">
<option value="0">없음</option>
<option value="30">1/64</option>
<option value="60">1/32</option>
<option value="120" selected>1/16</option>
<option value="240">1/8</option>
<option value="480">1/4</option>
<option value="960">1/2</option>
<option value="1920">1/1</option>
</select>
<div class="tb-sep"></div>
<span class="tb-lbl">BPM</span>
<input class="tb-num" id="bpm-in" type="number" value="120" min="20" max="300" onchange="setBpm(+this.value)">
<div class="tb-sep"></div>
<button class="tb-btn" onclick="zoomX(1.3)">X+</button>
<button class="tb-btn" onclick="zoomX(1/1.3)">X−</button>
<button class="tb-btn" onclick="zoomY(1.3)">Y+</button>
<button class="tb-btn" onclick="zoomY(1/1.3)">Y−</button>
<div class="tb-sep"></div>
<button class="tb-btn" onclick="doAddTrack()">+ 트랙</button>
<div class="tb-sep"></div>
<span class="tb-lbl">MIDI 출력</span>
<select id="midi-out-sel"><option value="__sf2">내장 SF2</option><option value="">-- 없음 --</option></select>
<button class="tb-btn" onclick="meRefreshMidiOuts()">↻</button>
</div>
<div id="me-main">
<div id="track-panel">
<div id="tp-head"><span>트랙</span></div>
<div id="track-list"></div>
</div>
<div id="roll-wrap">
<div id="ruler-row">
<div id="corner"><span>bar</span></div>
<canvas id="ruler-canvas" height="24"></canvas>
</div>
<div id="wave-area">
<div id="wave-lanes"></div>
<div id="audio-settings-panel" style="display:none;flex-wrap:wrap;gap:10px;padding:6px 10px;background:rgba(255,255,255,.03);border-top:1px solid rgba(255,255,255,.07);align-items:center;font-size:11px;color:#c0bede;">
<span style="font-size:10px;color:#5a5878;letter-spacing:.04em;">입력 장치</span>
<select id="audio-input-sel" style="font-size:10px;background:#1f2032;color:#d8daf0;border:1px solid rgba(255,255,255,.14);border-radius:4px;padding:2px 4px;max-width:180px" onchange="meAudioSettings.deviceId=this.value"></select>
<button class="wave-add-btn" style="padding:2px 7px" onclick="meEnumerateDevices()">↻</button>
<div class="tb-sep" style="height:16px;align-self:center"></div>
<span style="font-size:10px;color:#5a5878;">레이턴시 보정</span>
<input type="number" id="audio-latency-ms" value="0" min="-500" max="500" step="5" style="width:54px;font-size:10px;background:#1f2032;color:#d8daf0;border:1px solid rgba(255,255,255,.14);border-radius:4px;padding:2px 4px;text-align:right" oninput="meAudioSettings.latencyMs=+this.value"> <span style="font-size:10px;color:#5a5878;">ms</span>
<div class="tb-sep" style="height:16px;align-self:center"></div>
<label style="display:inline-flex;align-items:center;gap:3px;font-size:10px;cursor:pointer"><input type="checkbox" id="au-echo" onchange="meAudioSettings.echoCancellation=this.checked"> 에코제거</label>
<label style="display:inline-flex;align-items:center;gap:3px;font-size:10px;cursor:pointer"><input type="checkbox" id="au-noise" onchange="meAudioSettings.noiseSuppression=this.checked"> 노이즈제거</label>
<label style="display:inline-flex;align-items:center;gap:3px;font-size:10px;cursor:pointer"><input type="checkbox" id="au-agc" onchange="meAudioSettings.autoGainControl=this.checked"> 자동게인</label>
<div class="tb-sep" style="height:16px;align-self:center"></div>
<span style="font-size:10px;color:#5a5878;">측정 레이턴시</span>
<span id="audio-latency-info" style="font-size:10px;color:#a78bfa;">—</span>
<button class="wave-add-btn" style="padding:2px 7px" onclick="meMeasureLatency()">측정</button>
</div>
<div id="wave-add-bar">
<button class="wave-add-btn" onclick="document.getElementById('audio-file-in').click()">+ 트랙</button>
<button class="wave-add-btn" onclick="meAddRecordTrack()">+ 녹음</button>
<button class="wave-add-btn" onclick="meToggleAudioSettings()" title="오디오 입력 설정">⚙</button>
</div>
</div>
<div id="roll-row">
<canvas id="keys-canvas" width="56"></canvas>
<canvas id="roll-canvas"></canvas>
</div>
<div id="vel-row">
<div id="vel-label">vel</div>
<canvas id="vel-canvas" height="70"></canvas>
</div>
</div>
</div>
<div id="master-channel">
<span class="mc-label">MASTER</span>
<div class="mc-sep"></div>
<span class="mc-lbl">볼륨</span>
<input class="mc-sl" type="range" min="0" max="200" value="100" oninput="setMasterVol(this.value);this.nextElementSibling.textContent=this.value+'%'">
<span class="mc-val">100%</span>
<div class="mc-sep"></div>
<label class="mc-lbl"><input type="checkbox" onchange="setFxOn('reverb',this.checked)"> 리버브</label>
<input class="mc-sl" type="range" min="0" max="100" value="30" oninput="setFxMix('reverb',this.value);this.nextElementSibling.textContent=this.value">
<span class="mc-val">30</span>
<div class="mc-sep"></div>
<label class="mc-lbl"><input type="checkbox" onchange="setFxOn('chorus',this.checked)"> 코러스</label>
<input class="mc-sl" type="range" min="0" max="100" value="30" oninput="setFxMix('chorus',this.value);this.nextElementSibling.textContent=this.value">
<span class="mc-val">30</span>
<div class="mc-sep"></div>
<label class="mc-lbl"><input type="checkbox" onchange="setFxOn('delay',this.checked)"> 딜레이</label>
<span class="mc-lbl" style="color:#5a5878">믹스</span>
<input class="mc-sl" type="range" min="0" max="100" value="25" oninput="setFxMix('delay',this.value);this.nextElementSibling.textContent=this.value">
<span class="mc-val">25</span>
<span class="mc-lbl" style="color:#5a5878">시간</span>
<input class="mc-sl" type="range" min="50" max="800" step="10" value="300" oninput="setFxTime(this.value);this.nextElementSibling.textContent=this.value+'ms'">
<span class="mc-val">300ms</span>
<div class="mc-sep"></div>
<label class="mc-lbl"><input type="checkbox" onchange="setFxOn('comp',this.checked)"> 컴프</label>
<input class="mc-sl" type="range" min="0" max="100" value="40" oninput="setFxComp(this.value);this.nextElementSibling.textContent=this.value">
<span class="mc-val">40</span>
</div>
<div id="transport">
<button class="tr-btn" onclick="doRewind()">⏮</button>
<button class="tr-btn" id="play-btn" onclick="doTogglePlay()">▶</button>
<button class="tr-btn" onclick="doStop()">■</button>
<span id="pos-disp">1:1:000</span>
<span class="tb-lbl">루프</span>
<input type="checkbox" id="loop-en">
<span class="tb-lbl">In</span><input class="tb-num" id="loop-in" type="number" value="1" min="1" style="width:42px">
<span class="tb-lbl">Out</span><input class="tb-num" id="loop-out" type="number" value="5" min="1" style="width:42px">
<span style="flex:1"></span>
<span id="status-txt"></span>
</div>
</div>
<input type="file" id="file-in" accept=".mid,.midi" style="display:none" onchange="handleOpen(this)">
<input type="file" id="audio-file-in" accept="audio/*,.wav,.mp3,.flac,.ogg,.aac" style="display:none" onchange="meLoadAudio(this.files[0]);this.value=''">
</div>
<!-- Guitar Pro viewer -->
<div id="stage-gp">
<div class="gp-bar">
<button class="gp-ctrl-btn" onclick="closeGpViewer()">← 스테이지로</button>
<span class="gp-bar-title" id="gp-title">Guitar Pro</span>
<button class="gp-ctrl-btn" id="gp-play-btn" onclick="gpTogglePlay()">▶ 재생</button>
<button class="gp-ctrl-btn" onclick="gpStop()">■ 정지</button>
</div>
<div class="gp-fx-bar">
<span class="gp-fx-label">📤 MIDI 출력</span>
<select id="gp-midi-out" class="gp-ctrl-btn" style="padding:2px 6px;font-size:11px;" onfocus="initMidiOut()" onchange="selectMidiOut(this.value)">
<option value="">-- 내장 SF2 --</option>
</select>
<span class="gp-fx-label" style="margin-left:12px;">♩=</span>
<input type="number" id="gp-tempo" min="20" max="400" step="1"
onchange="setGpTempo(this.value)" onkeydown="if(event.key==='Enter')setGpTempo(this.value)" title="템포 (BPM)">
<span style="font-size:10px;color:var(--muted);">BPM</span>
<span class="gp-fx-label" style="margin-left:12px;">🔊 마스터</span>
<input type="range" id="gp-master-vol" min="0" max="200" value="100" onchange="setGpMasterVol(this.value)" title="마스터 볼륨 (최대 200%)" style="width:80px;vertical-align:middle;">
<span id="gp-master-vol-val" style="font-size:10px;color:var(--muted);min-width:34px;">100%</span>
<button class="gp-ctrl-btn" style="padding:2px 8px;font-size:11px;margin-left:8px;" onclick="toggleGpFxPanel()" title="마스터 효과 (리버브·코러스·딜레이·컴프레션)">🎛 효과</button>
</div>
<div id="gp-fx-panel" style="display:none;flex-wrap:wrap;gap:14px;padding:8px 12px;border-bottom:1px solid var(--line);background:var(--panel);align-items:center;font-size:11px;">
<span class="gp-fx-label">마스터 효과 — GP·드럼·루퍼 모든 소리에 적용</span>
<label class="gp-fx-e"><input type="checkbox" onchange="setFxOn('reverb',this.checked)"> 리버브</label>
<input type="range" class="gp-vol" min="0" max="100" value="30" oninput="setFxMix('reverb',this.value);this.nextElementSibling.textContent=this.value"><span class="gp-fx-val">30</span>
<label class="gp-fx-e"><input type="checkbox" onchange="setFxOn('chorus',this.checked)"> 코러스</label>
<input type="range" class="gp-vol" min="0" max="100" value="30" oninput="setFxMix('chorus',this.value);this.nextElementSibling.textContent=this.value"><span class="gp-fx-val">30</span>
<label class="gp-fx-e"><input type="checkbox" onchange="setFxOn('delay',this.checked)"> 딜레이</label>
<span class="gp-fx-label">믹스</span>
<input type="range" class="gp-vol" min="0" max="100" value="25" oninput="setFxMix('delay',this.value);this.nextElementSibling.textContent=this.value"><span class="gp-fx-val">25</span>
<span class="gp-fx-label">시간ms</span>
<input type="range" class="gp-vol" min="50" max="800" step="10" value="300" oninput="setFxTime(this.value);this.nextElementSibling.textContent=this.value"><span class="gp-fx-val">300</span>
<label class="gp-fx-e"><input type="checkbox" onchange="setFxOn('comp',this.checked)"> 컴프레션</label>
<input type="range" class="gp-vol" min="0" max="100" value="40" oninput="setFxComp(this.value);this.nextElementSibling.textContent=this.value"><span class="gp-fx-val">40</span>
</div>
<div class="gp-track-list" id="gp-track-list"></div>
<div class="gp-viewport" id="gp-viewport">
<div class="gp-loading" id="gp-loading">Guitar Pro 파일을 로딩 중...</div>
<div id="gp-main"></div>
</div>
</div>
<!-- Drum box -->
<div id="stage-drums">
<div class="gp-bar">
<button class="drum-back-btn" onclick="closeDrums()">← 스테이지로</button>
<span class="gp-bar-title">🥁 드럼 박스</span>
<span id="drum-sample-status" class="gp-fx-label" style="opacity:.7">샘플 로딩 전</span>
<button class="gp-ctrl-btn" id="drum-play-btn" onclick="drumTogglePlay()" style="background:rgba(34,197,94,.15);border-color:#4ade80;color:#86efac;">▶ 재생</button>
<button class="gp-ctrl-btn" onclick="drumStop()" style="background:rgba(239,68,68,.15);border-color:#f87171;color:#fca5a5;">■ 정지</button>
<span class="gp-fx-label" style="margin-left:8px;">♩=</span>
<input type="number" id="drum-bpm" min="40" max="300" step="1" value="120"
onchange="drumSetBpm(this.value)" onkeydown="if(event.key==='Enter')drumSetBpm(this.value)" title="BPM" style="background:transparent;border:none;border-bottom:1px solid var(--studio-border2);color:var(--studio-text);font-size:13px;font-family:inherit;padding:1px 2px;text-align:center;outline:none;width:44px;">
<span style="font-size:10px;color:var(--studio-muted);">BPM</span>
<span class="gp-fx-label" style="margin-left:8px;">🔊</span>
<input type="range" id="drum-vol" min="0" max="100" value="80" onchange="drumSetVol(this.value)" style="width:80px;vertical-align:middle;">
<div style="flex:1"></div>
<select id="drum-steps" class="gp-ctrl-btn" style="padding:2px 6px;font-size:11px;" onchange="drumSetSteps(this.value)">
<option value="16">16 스텝</option>
<option value="32">32 스텝</option>
</select>
<button class="gp-ctrl-btn" onclick="drumClear()">지움</button>
<button class="gp-ctrl-btn" onclick="drumExportMidi()" style="border-color:#22c55e;color:#86efac;">⬇ MIDI</button>
</div>
<div class="gp-fx-bar" style="flex-wrap:wrap;gap:8px;">
<span class="gp-fx-label">리듬</span>
<select id="drum-rhythm" class="gp-ctrl-btn" style="padding:2px 6px;font-size:11px;" onchange="drumLoadRhythm(this.value)"></select>
<span class="gp-fx-label" style="margin-left:6px;">스윙</span>
<input type="range" id="drum-swing" min="0" max="70" value="0" onchange="drumSetSwing(this.value)" style="width:90px;vertical-align:middle;">
<span id="drum-swing-val" class="gp-fx-label" style="min-width:64px;">0% (8분)</span>
<div style="flex:1"></div>
<span class="gp-fx-label">저장된 패턴</span>
<select id="drum-saved" class="gp-ctrl-btn" style="padding:2px 6px;font-size:11px;min-width:120px;"><option value="">— 없음 —</option></select>
<button class="gp-ctrl-btn" onclick="drumLoadSavedSel()">불러오기</button>
<button class="gp-ctrl-btn" onclick="drumSavePattern()" style="border-color:#22c55e;color:#86efac;">💾 저장</button>
<button class="gp-ctrl-btn" onclick="drumDeleteSavedSel()" style="border-color:#ef4444;color:#fca5a5;">삭제</button>
</div>
<div id="drum-grid" class="drum-grid"></div>
<!-- Looper built into drum box (beat-synced) -->
<div class="drum-loop-panel">
<div class="loop-card">
<div class="loop-card-hdr">
<span class="loop-card-title">🎤 루퍼</span>
<span id="loop-status" class="loop-card-status">레이어 0 · 대기</span>
</div>
<div class="loop-meter-bar"><div id="loop-meter-fill" class="loop-meter-fill"></div></div>
<div class="loop-transport">
<button class="loop-btn rec" id="loop-rec-btn" onclick="loopToggleRecord()">
<span class="lb-icon">⏺</span>녹음
</button>
<button class="loop-btn" id="loop-play-btn" onclick="loopTogglePlay()">
<span class="lb-icon">▶</span>재생
</button>
<button class="loop-btn" onclick="loopStop()">
<span class="lb-icon">⏹</span>정지
</button>
</div>
<div class="loop-sec">
<span style="font-size:11px;color:var(--muted);">길이</span>
<select id="loop-bars" onchange="loopSetBars(this.value)">
<option value="1">1 마디</option>
<option value="2">2 마디</option>
<option value="4">4 마디</option>
<option value="8">8 마디</option>
</select>
<button onclick="loopUndo()">↶ 취소</button>
<button class="loop-del-all" onclick="loopClear()">🗑 전체 삭제</button>
</div>
<div id="loop-layers" class="loop-layers">
<div class="loop-empty">⏺ 녹음을 누르면 드럼 비트에 맞춰 녹음 시작<br>(1마디 카운트인 후 시작 · 재생 중 누르면 오버덥)</div>
</div>
</div>
</div>
</div>
</section>
<!-- ── Resize handle ── -->
<div class="ms-resize" id="resize-handle"></div>
<!-- ── Right chat panel ── -->
<div class="ms-chat" id="chat-panel">
<div class="ms-chat-hdr">
<span class="ms-chat-hdr-label">🎵 AI</span>
<select class="ms-model-sel" id="model-sel" onchange="saveModel()">
<option value="">기본 모델</option>
</select>
<button class="ms-chat-clear" onclick="clearChat()" title="대화 지우기">🗑</button>
</div>
<div class="ms-msgs" id="chat-msgs"></div>
<div class="ms-input-bar">
<textarea class="ms-textarea" id="chat-input" rows="1" placeholder="음악에 대해 물어보세요..." onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
<button class="ms-send" id="send-btn" onclick="sendMessage()">전송</button>
</div>
</div>
</div>
<script src="/js/music/music-app.js"></script>
<!-- ── Folder popup ── -->
<div id="fp-overlay" onclick="if(event.target===this)closeFolderPopup()">
<div id="fp-sheet">
<div class="fp-hdr">
<span class="fp-title">📂 저장된 파일</span>
<button class="fp-icon-btn" onclick="fpReload()" title="새로고침">↺</button>
<button class="fp-icon-btn" onclick="closeFolderPopup()" style="font-size:16px;">✕</button>
</div>
<div class="fp-tabs">
<button class="fp-tab active" id="fp-tab-midi" onclick="fpSetTab('midi')">🎵 MIDI</button>
<button class="fp-tab" id="fp-tab-gp" onclick="fpSetTab('gp')">🎸 Guitar Pro</button>
</div>
<div class="fp-body">
<div id="fp-midi-body"><div class="fp-empty">로딩 중...</div></div>
<div id="fp-gp-body" style="display:none"><div class="fp-empty">로딩 중...</div></div>
</div>
</div>
</div>
<!-- AudioMass scripts (loaded but not initialized until panel opens) -->
<script src="/audiomass/dist/wavesurfer.js"></script>
<script src="/audiomass/dist/plugin/wavesurfer.regions.js"></script>
<script src="/audiomass/oneup.js"></script>
<script src="/audiomass/app.js"></script>
<script src="/audiomass/keys.js"></script>
<script src="/audiomass/markers.js"></script>
<script src="/audiomass/contextmenu.js"></script>
<script src="/audiomass/lufs.js"></script>
<script src="/audiomass/ui-fx.js"></script>
<script src="/audiomass/ui.js"></script>
<script src="/audiomass/modal.js"></script>
<script src="/audiomass/state.js"></script>
<script src="/audiomass/engine.js"></script>
<script src="/audiomass/actions.js"></script>
<script src="/audiomass/drag.js"></script>
<script src="/audiomass/recorder.js"></script>
<!-- welcome.js omitted: shows unwanted popup when embedded -->
<script src="/audiomass/fx-pg-eq.js"></script>
<script src="/audiomass/fx-auto.js"></script>
<script src="/audiomass/local.js"></script>
<script src="/audiomass/id3.js"></script>
<script src="/audiomass/lzma.js"></script>
<script src="/audiomass/amss-format.js"></script>
<script src="/audiomass/multitrack.js"></script>
</body>
</html>