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>
1198 lines
80 KiB
HTML
1198 lines
80 KiB
HTML
<!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>
|