@extends('layouts.app')

@section('title', '사운드 로고 생성기')

@section('content')
<style>
:root {
    --sl-blue: #3b82f6;
    --sl-indigo: #6366f1;
    --sl-purple: #8b5cf6;
    --sl-green: #10b981;
    --sl-amber: #f59e0b;
    --sl-red: #ef4444;
    --sl-bg: #0f172a;
    --sl-card: #1e293b;
    --sl-card2: #334155;
    --sl-border: #475569;
    --sl-text: #f1f5f9;
    --sl-text2: #94a3b8;
    --sl-radius: 10px;
}

.sl-wrap {
    display: flex; flex-direction: column; height: calc(100vh - 64px);
    background: var(--sl-bg); overflow: hidden;
    font-family: 'Pretendard', -apple-system, sans-serif; color: var(--sl-text);
    margin: -24px;
}

/* Toolbar */
.sl-toolbar {
    display: flex; align-items: center; height: 48px; padding: 0 16px;
    background: var(--sl-card); border-bottom: 1px solid var(--sl-border);
    gap: 12px; flex-shrink: 0;
}
.sl-toolbar h1 { font-size: 14px; font-weight: 700; margin: 0; display: flex; align-items: center; gap: 6px; }
.sl-toolbar h1 i { color: var(--sl-purple); }

/* Buttons */
.sl-btn {
    display: inline-flex; align-items: center; gap: 5px; padding: 6px 12px;
    border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer;
    border: 1px solid var(--sl-border); background: var(--sl-card2); color: var(--sl-text);
    transition: all .15s;
}
.sl-btn:hover { background: #475569; }
.sl-btn.primary { background: var(--sl-indigo); border-color: var(--sl-indigo); color: #fff; }
.sl-btn.primary:hover { background: #4f46e5; }
.sl-btn.success { background: var(--sl-green); border-color: var(--sl-green); color: #fff; }
.sl-btn.danger { background: var(--sl-red); border-color: var(--sl-red); color: #fff; }
.sl-btn.sm { padding: 4px 8px; font-size: 11px; }
.sl-btn.lg { padding: 10px 20px; font-size: 14px; }
.sl-btn.play { background: var(--sl-green); border-color: var(--sl-green); color: #fff; min-width: 80px; justify-content: center; }
.sl-btn.play:hover { background: #059669; }
.sl-btn.stop { background: var(--sl-red); border-color: var(--sl-red); color: #fff; }

/* Tabs */
.sl-tabs { display: flex; gap: 2px; background: var(--sl-card); border-radius: 8px; padding: 3px; }
.sl-tab {
    padding: 6px 14px; border-radius: 6px; font-size: 12px; font-weight: 500;
    cursor: pointer; color: var(--sl-text2); transition: all .15s; border: none; background: none;
}
.sl-tab.active { background: var(--sl-indigo); color: #fff; }
.sl-tab:hover:not(.active) { color: var(--sl-text); background: var(--sl-card2); }

/* Main Layout */
.sl-main {
    display: flex; flex: 1; overflow: hidden; min-height: 0;
}
.sl-sidebar {
    width: 240px; flex-shrink: 0; background: var(--sl-card);
    border-right: 1px solid var(--sl-border); overflow-y: auto; padding: 12px;
}
.sl-content {
    flex: 1; overflow-y: auto; padding: 20px;
}

/* Section */
.sl-section { margin-bottom: 20px; }
.sl-section-title {
    font-size: 11px; font-weight: 600; color: var(--sl-text2);
    text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;
}

/* Note Sequencer */
.sl-sequencer {
    display: flex; gap: 6px; flex-wrap: wrap; align-items: flex-end;
    background: var(--sl-card); border-radius: var(--sl-radius); padding: 16px;
    border: 1px solid var(--sl-border); min-height: 120px;
}
.sl-note {
    display: flex; flex-direction: column; align-items: center; gap: 4px; cursor: pointer;
    padding: 8px 6px; border-radius: 8px; border: 2px solid transparent;
    transition: all .15s; position: relative; min-width: 52px;
}
.sl-note:hover { border-color: var(--sl-indigo); }
.sl-note.active { border-color: var(--sl-green); background: rgba(16,185,129,.1); }
.sl-note.playing { border-color: var(--sl-amber); background: rgba(245,158,11,.15); box-shadow: 0 0 12px rgba(245,158,11,.3); }
.sl-note-bar {
    width: 36px; border-radius: 4px 4px 0 0; background: var(--sl-indigo);
    transition: height .2s;
}
.sl-note-label { font-size: 11px; font-weight: 600; color: var(--sl-text); }
.sl-note-dur { font-size: 9px; color: var(--sl-text2); }
.sl-note-del {
    position: absolute; top: 2px; right: 2px; width: 16px; height: 16px;
    border-radius: 50%; background: var(--sl-red); color: #fff; font-size: 10px;
    display: none; align-items: center; justify-content: center; cursor: pointer; border: none;
}
.sl-note:hover .sl-note-del { display: flex; }

/* Rest */
.sl-rest .sl-note-bar { background: var(--sl-card2); border: 1px dashed var(--sl-border); }
.sl-rest .sl-note-label { color: var(--sl-text2); }

/* Chord */
.sl-chord .sl-note-bar { background: linear-gradient(135deg, var(--sl-purple), var(--sl-indigo)); }

/* Add Note Button */
.sl-add-note {
    width: 44px; min-height: 80px; border: 2px dashed var(--sl-border); border-radius: 8px;
    display: flex; align-items: center; justify-content: center; cursor: pointer;
    color: var(--sl-text2); font-size: 18px; transition: all .15s;
}
.sl-add-note:hover { border-color: var(--sl-indigo); color: var(--sl-indigo); }

/* Param Sliders */
.sl-param { margin-bottom: 12px; }
.sl-param-label {
    display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 4px;
}
.sl-param-label span:first-child { color: var(--sl-text2); }
.sl-param-label span:last-child { color: var(--sl-text); font-weight: 600; }
.sl-slider {
    width: 100%; -webkit-appearance: none; height: 4px; border-radius: 2px;
    background: var(--sl-card2); outline: none;
}
.sl-slider::-webkit-slider-thumb {
    -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%;
    background: var(--sl-indigo); cursor: pointer; border: 2px solid var(--sl-bg);
}

/* Waveform Canvas */
.sl-waveform {
    width: 100%; height: 80px; border-radius: var(--sl-radius);
    background: var(--sl-card); border: 1px solid var(--sl-border);
}

/* Preset List */
.sl-preset {
    padding: 8px 10px; border-radius: 6px; cursor: pointer; margin-bottom: 4px;
    display: flex; align-items: center; gap: 8px; font-size: 12px;
    color: var(--sl-text2); transition: all .15s;
}
.sl-preset:hover { background: var(--sl-card2); color: var(--sl-text); }
.sl-preset.active { background: var(--sl-indigo); color: #fff; }
.sl-preset i { font-size: 14px; }

/* Project List */
.sl-proj {
    padding: 6px 8px; border-radius: 6px; cursor: pointer; margin-bottom: 3px;
    display: flex; align-items: center; justify-content: space-between;
    font-size: 11px; color: var(--sl-text2); transition: all .15s;
}
.sl-proj:hover { background: var(--sl-card2); }
.sl-proj.active { background: rgba(99,102,241,.2); color: var(--sl-text); }

/* Select / Input */
.sl-select, .sl-input {
    width: 100%; padding: 6px 10px; border-radius: 6px; font-size: 12px;
    background: var(--sl-card2); border: 1px solid var(--sl-border); color: var(--sl-text);
    outline: none;
}
.sl-select:focus, .sl-input:focus { border-color: var(--sl-indigo); }

/* Toast */
.sl-toast {
    position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
    color: #fff; padding: 14px 28px; max-width: 420px; text-align: center;
    border-radius: 10px; font-size: 13px; font-weight: 500; z-index: 9999;
    box-shadow: 0 8px 24px rgba(0,0,0,.5); line-height: 1.5;
}
.sl-toast.info { background: var(--sl-indigo); }
.sl-toast.warn { background: #d97706; }
.sl-toast.error { background: var(--sl-red); }

/* Note picker dropdown */
.sl-note-picker {
    position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
    background: var(--sl-card); border: 1px solid var(--sl-border); border-radius: 8px;
    padding: 8px; z-index: 100; box-shadow: 0 4px 12px rgba(0,0,0,.4);
    display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; min-width: 220px;
}
.sl-note-picker button {
    padding: 4px 2px; border-radius: 4px; border: none; font-size: 10px; font-weight: 600;
    cursor: pointer; background: var(--sl-card2); color: var(--sl-text);
}
.sl-note-picker button:hover { background: var(--sl-indigo); }
.sl-note-picker button.black { background: #1a1a2e; color: #a5b4fc; }
.sl-note-picker button.black:hover { background: var(--sl-purple); color: #fff; }

/* Transport */
.sl-transport {
    display: flex; align-items: center; gap: 10px; padding: 12px 16px;
    background: var(--sl-card); border-top: 1px solid var(--sl-border); flex-shrink: 0;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* Help Modal */
.sl-modal-overlay {
    position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 9000;
    display: flex; align-items: center; justify-content: center;
    backdrop-filter: blur(4px);
}
.sl-modal {
    background: var(--sl-card); border: 1px solid var(--sl-border); border-radius: 14px;
    width: 90%; max-width: 760px; max-height: 85vh; display: flex; flex-direction: column;
    box-shadow: 0 20px 60px rgba(0,0,0,.5);
}
.sl-modal-header {
    display: flex; align-items: center; gap: 10px; padding: 16px 20px;
    border-bottom: 1px solid var(--sl-border); flex-shrink: 0;
}
.sl-modal-header h2 { font-size: 16px; font-weight: 700; margin: 0; flex: 1; }
.sl-modal-body {
    overflow-y: auto; padding: 20px; flex: 1;
    font-size: 13px; line-height: 1.8; color: var(--sl-text2);
}
.sl-modal-body h3 {
    font-size: 14px; font-weight: 700; color: var(--sl-text); margin: 20px 0 8px;
    display: flex; align-items: center; gap: 6px;
}
.sl-modal-body h3:first-child { margin-top: 0; }
.sl-modal-body strong { color: var(--sl-text); }
.sl-modal-body .help-badge {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600;
}
.sl-modal-body .help-table {
    width: 100%; border-collapse: collapse; margin: 8px 0 12px; font-size: 12px;
}
.sl-modal-body .help-table th,
.sl-modal-body .help-table td {
    padding: 6px 10px; border: 1px solid var(--sl-border); text-align: left;
}
.sl-modal-body .help-table th {
    background: var(--sl-bg); color: var(--sl-text); font-weight: 600; white-space: nowrap;
}
.sl-modal-body .help-step {
    display: flex; gap: 10px; margin-bottom: 10px;
}
.sl-modal-body .help-step-num {
    width: 24px; height: 24px; border-radius: 50%; background: var(--sl-indigo); color: #fff;
    display: flex; align-items: center; justify-content: center;
    font-size: 12px; font-weight: 700; flex-shrink: 0; margin-top: 1px;
}
.sl-modal-body .help-step-text { flex: 1; }
.sl-modal-body .help-divider {
    border: none; border-top: 1px solid var(--sl-border); margin: 16px 0;
}
</style>

<div x-data="soundLogo()" x-init="init()" class="sl-wrap">
    <!-- Toolbar -->
    <div class="sl-toolbar">
        <h1><i class="ri-music-2-line"></i> 사운드 로고 생성기</h1>
        <button class="sl-btn sm" @click="showHelp = true" title="사용법 안내"
                style="padding: 4px 8px; border-radius: 50%; width: 28px; height: 28px; justify-content: center;">
            <i class="ri-question-line" style="font-size: 14px;"></i>
        </button>
        <div style="flex:1;"></div>

        <div class="sl-tabs">
            <button class="sl-tab" :class="mode === 'manual' && 'active'" @click="mode = 'manual'">
                <i class="ri-piano-line"></i> 수동
            </button>
            <button class="sl-tab" :class="mode === 'preset' && 'active'" @click="mode = 'preset'">
                <i class="ri-magic-line"></i> 프리셋
            </button>
            <button class="sl-tab" :class="mode === 'ai' && 'active'" @click="mode = 'ai'" style="position: relative;">
                <i class="ri-sparkling-line"></i> AI 생성
            </button>
            <button class="sl-tab" :class="mode === 'bgm' && 'active'" @click="mode = 'bgm'" style="position: relative;">
                <i class="ri-music-ai-line"></i> AI 배경음악
                <span style="position: absolute; top: -2px; right: -2px; font-size: 8px; background: var(--sl-green); color: #fff; padding: 1px 4px; border-radius: 4px; font-weight: 700;">Lyria</span>
            </button>
        </div>

        <div style="flex:1;"></div>
        <span style="font-size: 11px; color: var(--sl-text2);">
            <i class="ri-music-line"></i> <span x-text="notes.length"></span>개 음표 |
            <span x-text="getTotalDuration().toFixed(2)"></span>초
            <template x-if="voiceBuffer">
                <span style="color: var(--sl-green);"> | <i class="ri-mic-fill"></i> 음성</span>
            </template>
            <template x-if="bgmBuffer">
                <span style="color: var(--sl-amber);"> | <i class="ri-music-ai-line"></i> 배경음악</span>
            </template>
        </span>
    </div>

    <!-- Main -->
    <div class="sl-main">
        <!-- Sidebar -->
        <div class="sl-sidebar">
            <!-- Synth Type -->
            <div class="sl-section">
                <div class="sl-section-title">음색 (Synthesizer)</div>
                <template x-for="s in synthTypes" :key="s.code">
                    <div class="sl-preset" :class="synth === s.code && 'active'" @click="synth = s.code">
                        <span x-text="s.icon"></span>
                        <span x-text="s.label"></span>
                    </div>
                </template>
            </div>

            <!-- ADSR -->
            <div class="sl-section">
                <div class="sl-section-title">엔벨로프 (ADSR)</div>
                <div class="sl-param">
                    <div class="sl-param-label"><span>Attack</span><span x-text="adsr.attack + 'ms'"></span></div>
                    <input type="range" class="sl-slider" min="1" max="500" x-model.number="adsr.attack">
                </div>
                <div class="sl-param">
                    <div class="sl-param-label"><span>Decay</span><span x-text="adsr.decay + 'ms'"></span></div>
                    <input type="range" class="sl-slider" min="1" max="1000" x-model.number="adsr.decay">
                </div>
                <div class="sl-param">
                    <div class="sl-param-label"><span>Sustain</span><span x-text="(adsr.sustain * 100).toFixed(0) + '%'"></span></div>
                    <input type="range" class="sl-slider" min="0" max="100" :value="adsr.sustain * 100" @input="adsr.sustain = $event.target.value / 100">
                </div>
                <div class="sl-param">
                    <div class="sl-param-label"><span>Release</span><span x-text="adsr.release + 'ms'"></span></div>
                    <input type="range" class="sl-slider" min="10" max="3000" x-model.number="adsr.release">
                </div>
            </div>

            <!-- Effects -->
            <div class="sl-section">
                <div class="sl-section-title">이펙트</div>
                <div class="sl-param">
                    <div class="sl-param-label"><span>볼륨</span><span x-text="(volume * 100).toFixed(0) + '%'"></span></div>
                    <input type="range" class="sl-slider" min="0" max="100" :value="volume * 100" @input="volume = $event.target.value / 100">
                </div>
                <div class="sl-param">
                    <div class="sl-param-label"><span>리버브</span><span x-text="(reverb * 100).toFixed(0) + '%'"></span></div>
                    <input type="range" class="sl-slider" min="0" max="100" :value="reverb * 100" @input="reverb = $event.target.value / 100">
                </div>
            </div>

            <!-- Voice Overlay -->
            <div class="sl-section">
                <div class="sl-section-title">음성 오버레이 (TTS)</div>
                <div style="margin-bottom: 6px;">
                    <input class="sl-input" x-model="voiceText" placeholder='예: 우리들의 솔루션 쌤' style="margin-bottom: 6px;">

                    <!-- 성별/연령 카테고리 -->
                    <div style="display: flex; gap: 2px; margin-bottom: 6px; background: var(--sl-bg); border-radius: 6px; padding: 2px;">
                        <button style="flex:1; padding: 4px 0; border-radius: 5px; border: none; font-size: 10px; font-weight: 600; cursor: pointer; transition: all .15s;"
                                :style="voiceCategory === 'female' ? 'background: #ec4899; color: #fff;' : 'background: transparent; color: var(--sl-text2);'"
                                @click="voiceCategory = 'female'; voiceName = voiceGroups.female[0].value;">
                            <i class="ri-women-line"></i> 여성
                        </button>
                        <button style="flex:1; padding: 4px 0; border-radius: 5px; border: none; font-size: 10px; font-weight: 600; cursor: pointer; transition: all .15s;"
                                :style="voiceCategory === 'male' ? 'background: #3b82f6; color: #fff;' : 'background: transparent; color: var(--sl-text2);'"
                                @click="voiceCategory = 'male'; voiceName = voiceGroups.male[0].value;">
                            <i class="ri-men-line"></i> 남성
                        </button>
                        <button style="flex:1; padding: 4px 0; border-radius: 5px; border: none; font-size: 10px; font-weight: 600; cursor: pointer; transition: all .15s;"
                                :style="voiceCategory === 'child' ? 'background: #f59e0b; color: #fff;' : 'background: transparent; color: var(--sl-text2);'"
                                @click="voiceCategory = 'child'; voiceName = voiceGroups.child[0].value;">
                            <i class="ri-emotion-happy-line"></i> 아이
                        </button>
                    </div>

                    <!-- 음성 선택 -->
                    <select class="sl-select" x-model="voiceName" style="margin-bottom: 6px;">
                        <template x-for="opt in voiceGroups[voiceCategory]" :key="opt.value">
                            <option :value="opt.value" x-text="opt.label"></option>
                        </template>
                    </select>

                    <!-- 톤/스타일 + 속도 -->
                    <div style="display: flex; gap: 4px; margin-bottom: 6px;">
                        <select class="sl-select" x-model="voiceStyle" style="flex: 1;">
                            <template x-for="s in voiceStyleOptions" :key="s.value">
                                <option :value="s.value" x-text="s.label"></option>
                            </template>
                        </select>
                    </div>

                    <!-- 말하기 속도 -->
                    <div class="sl-param" style="margin-bottom: 8px;">
                        <div class="sl-param-label">
                            <span><i class="ri-speed-line"></i> 속도</span>
                            <span x-text="voiceSpeedLabels[voiceSpeed - 1]" style="font-size: 10px;"></span>
                        </div>
                        <input type="range" class="sl-slider" min="1" max="5" step="1"
                               x-model.number="voiceSpeed">
                    </div>

                    <button class="sl-btn sm primary" style="width:100%;" @click="generateVoice()" :disabled="voiceLoading || !voiceText.trim()">
                        <template x-if="!voiceLoading">
                            <span><i class="ri-mic-line"></i> 음성 생성</span>
                        </template>
                        <template x-if="voiceLoading">
                            <span><i class="ri-loader-4-line" style="animation: spin 1s linear infinite;"></i> 생성 중...</span>
                        </template>
                    </button>
                </div>
                <template x-if="voiceBuffer">
                    <div>
                        <div style="padding: 6px 8px; border-radius: 6px; background: rgba(16,185,129,.1); border: 1px solid rgba(16,185,129,.3); margin-bottom: 8px;">
                            <div style="font-size: 11px; color: var(--sl-green); display: flex; align-items: center; gap: 4px;">
                                <i class="ri-checkbox-circle-fill"></i>
                                <span x-text="voiceText + ' · ' + voiceBuffer.duration.toFixed(1) + '초'"></span>
                            </div>
                        </div>
                        <div class="sl-param">
                            <div class="sl-param-label"><span>시작 시점</span><span x-text="voiceDelay.toFixed(1) + '초'"></span></div>
                            <input type="range" class="sl-slider" min="0" max="30" :value="voiceDelay * 10" @input="voiceDelay = $event.target.value / 10">
                        </div>
                        <div class="sl-param">
                            <div class="sl-param-label"><span>음성 볼륨</span><span x-text="(voiceVolume * 100).toFixed(0) + '%'"></span></div>
                            <input type="range" class="sl-slider" min="0" max="100" :value="voiceVolume * 100" @input="voiceVolume = $event.target.value / 100">
                        </div>
                        <div style="display: flex; gap: 4px;">
                            <button class="sl-btn sm" @click="playVoiceOnly()" style="flex:1;"><i class="ri-play-fill"></i> 음성만</button>
                            <button class="sl-btn sm danger" @click="clearVoice()" style="flex:1;"><i class="ri-delete-bin-line"></i> 삭제</button>
                        </div>
                    </div>
                </template>
            </div>

            <!-- Saved Sounds -->
            <div class="sl-section">
                <div class="sl-section-title">내 사운드</div>
                <template x-for="(snd, idx) in savedSounds" :key="snd.id">
                    <div class="sl-proj" :class="currentSoundId === snd.id && 'active'" @click="loadSound(snd)">
                        <span><i class="ri-music-line"></i> <span x-text="snd.name"></span></span>
                        <button class="sl-btn sm danger" style="padding:2px 5px;" @click.stop="deleteSound(idx)">&times;</button>
                    </div>
                </template>
                <button class="sl-btn sm" style="width:100%; margin-top: 6px;" @click="saveCurrentSound()">
                    <i class="ri-save-line"></i> 현재 사운드 저장
                </button>
            </div>
        </div>

        <!-- Content -->
        <div class="sl-content">
            <!-- Preset Mode -->
            <template x-if="mode === 'preset'">
                <div>
                    <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 14px; flex-wrap: wrap;">
                        <div class="sl-section-title" style="margin: 0;">프리셋 템플릿 (50종)</div>
                        <div style="display: flex; gap: 3px; flex-wrap: wrap;">
                            <template x-for="c in presetCategories" :key="c.code">
                                <button class="sl-btn sm"
                                        :class="presetCategory === c.code && 'primary'"
                                        @click="presetCategory = c.code"
                                        x-text="c.label + (c.code === 'all' ? ' (' + presets.length + ')' : ' (' + presets.filter(p => p.cat === c.code).length + ')')">
                                </button>
                            </template>
                        </div>
                    </div>
                    <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px;">
                        <template x-for="(p, i) in presets" :key="i">
                            <div x-show="presetCategory === 'all' || p.cat === presetCategory"
                                 style="background: var(--sl-card); border: 1px solid var(--sl-border); border-radius: 10px; padding: 14px; cursor: pointer; transition: all .15s;"
                                 :style="presetIdx === i ? 'border-color: var(--sl-indigo); box-shadow: 0 0 12px rgba(99,102,241,.3);' : ''"
                                 @click="loadPreset(i)">
                                <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
                                    <span style="font-size: 20px;" x-text="p.icon"></span>
                                    <div>
                                        <div style="font-size: 13px; font-weight: 600;" x-text="p.name"></div>
                                        <div style="font-size: 10px; color: var(--sl-text2);" x-text="p.style + ' · ' + p.duration + '초'"></div>
                                    </div>
                                </div>
                                <div style="font-size: 11px; color: var(--sl-text2);" x-text="p.desc"></div>
                            </div>
                        </template>
                    </div>
                </div>
            </template>

            <!-- AI Mode -->
            <template x-if="mode === 'ai'">
                <div>
                    <div style="max-width: 640px; margin: 0 auto;">
                        <!-- AI Header -->
                        <div style="text-align: center; margin-bottom: 24px;">
                            <div style="font-size: 36px; margin-bottom: 8px;">✨</div>
                            <h2 style="font-size: 18px; font-weight: 700; margin: 0 0 6px;">AI 사운드 로고 생성</h2>
                            <p style="font-size: 12px; color: var(--sl-text2); margin: 0;">원하는 사운드를 설명하면 AI가 음표 시퀀스를 설계합니다</p>
                        </div>

                        <!-- Prompt Input -->
                        <div class="sl-section">
                            <div class="sl-section-title">프롬프트</div>
                            <textarea class="sl-input" x-model="aiPrompt" rows="3"
                                      placeholder="예: 밝고 미래적인 IT 기업 시그널, 5음으로 상승하는 느낌"
                                      style="resize: vertical; min-height: 72px;"></textarea>
                        </div>

                        <!-- Options -->
                        <div style="display: flex; gap: 12px; margin-bottom: 16px;">
                            <div style="flex: 1;">
                                <div class="sl-section-title">카테고리</div>
                                <select class="sl-select" x-model="aiCategory">
                                    <option value="기업 시그널">기업 시그널</option>
                                    <option value="알림/메시지">알림/메시지</option>
                                    <option value="상태/피드백">상태/피드백</option>
                                    <option value="전환 효과">전환 효과</option>
                                    <option value="게임 효과">게임 효과</option>
                                    <option value="UI 인터랙션">UI 인터랙션</option>
                                    <option value="브랜드 징글">브랜드 징글</option>
                                    <option value="방송/미디어">방송/미디어</option>
                                </select>
                            </div>
                            <div style="flex: 1;">
                                <div class="sl-section-title">목표 길이</div>
                                <div style="display: flex; align-items: center; gap: 8px;">
                                    <input type="range" class="sl-slider" min="3" max="50" x-model.number="aiDurationRaw" style="flex:1;">
                                    <span style="font-size: 12px; font-weight: 600; min-width: 36px;" x-text="(aiDurationRaw / 10).toFixed(1) + '초'"></span>
                                </div>
                            </div>
                        </div>

                        <!-- Generate Button -->
                        <button class="sl-btn primary lg" style="width: 100%; justify-content: center; margin-bottom: 20px;"
                                @click="generateWithAi()" :disabled="aiLoading || !aiPrompt.trim()">
                            <template x-if="!aiLoading">
                                <span><i class="ri-sparkling-line"></i> AI로 생성하기</span>
                            </template>
                            <template x-if="aiLoading">
                                <span><i class="ri-loader-4-line" style="animation: spin 1s linear infinite;"></i> 생성 중...</span>
                            </template>
                        </button>

                        <!-- AI Result Preview -->
                        <template x-if="aiResult">
                            <div style="background: var(--sl-card); border: 1px solid var(--sl-green); border-radius: 10px; padding: 16px;">
                                <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
                                    <span style="font-size: 20px;">🎵</span>
                                    <div style="flex: 1;">
                                        <div style="font-size: 14px; font-weight: 600;" x-text="aiResult.name"></div>
                                        <div style="font-size: 11px; color: var(--sl-text2);" x-text="aiResult.desc"></div>
                                    </div>
                                    <span style="font-size: 11px; color: var(--sl-green); font-weight: 600;"
                                          x-text="aiResult.notes.length + '개 음표 · ' + aiResult.synth"></span>
                                </div>

                                <!-- Mini note preview -->
                                <div style="display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 12px; padding: 10px; background: var(--sl-bg); border-radius: 8px;">
                                    <template x-for="(n, i) in aiResult.notes" :key="i">
                                        <div style="padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;"
                                             :style="n.type === 'rest' ? 'background: var(--sl-card2); color: var(--sl-text2);' : n.type === 'chord' ? 'background: rgba(139,92,246,.2); color: #a78bfa;' : 'background: rgba(99,102,241,.2); color: #818cf8;'"
                                             x-text="n.type === 'rest' ? '쉼' : n.type === 'chord' ? (n.chord||[]).join('+') : n.note">
                                        </div>
                                    </template>
                                </div>

                                <div style="display: flex; gap: 8px;">
                                    <button class="sl-btn success" style="flex: 1; justify-content: center;" @click="loadAiResult()">
                                        <i class="ri-check-line"></i> 시퀀서에 로드
                                    </button>
                                    <button class="sl-btn play" @click="previewAiResult()">
                                        <i class="ri-play-fill"></i> 미리듣기
                                    </button>
                                    <button class="sl-btn" @click="generateWithAi()">
                                        <i class="ri-refresh-line"></i> 다시 생성
                                    </button>
                                </div>
                            </div>
                        </template>

                        <!-- AI Error -->
                        <template x-if="aiError">
                            <div style="background: rgba(239,68,68,.1); border: 1px solid var(--sl-red); border-radius: 10px; padding: 12px; margin-top: 12px;">
                                <div style="font-size: 12px; color: var(--sl-red);">
                                    <i class="ri-error-warning-line"></i> <span x-text="aiError"></span>
                                </div>
                            </div>
                        </template>

                        <!-- Quick Prompts -->
                        <div class="sl-section" style="margin-top: 20px;">
                            <div class="sl-section-title">빠른 프롬프트 예시</div>
                            <div style="display: flex; flex-wrap: wrap; gap: 6px;">
                                <template x-for="qp in aiQuickPrompts" :key="qp">
                                    <button class="sl-btn sm" @click="aiPrompt = qp" x-text="qp"></button>
                                </template>
                            </div>
                        </div>
                    </div>
                </div>
            </template>

            <!-- BGM Mode (Lyria RealTime) -->
            <template x-if="mode === 'bgm'">
                <div>
                    <div style="padding: 20px;">
                        <!-- Header -->
                        <div style="text-align: center; margin-bottom: 24px;">
                            <div style="font-size: 36px; margin-bottom: 8px;">🎼</div>
                            <h2 style="font-size: 18px; font-weight: 700; margin: 0 0 6px;">AI 배경음악 생성</h2>
                            <p style="font-size: 12px; color: var(--sl-text2); margin: 0;">Google Lyria RealTime — 텍스트 프롬프트로 다중 악기 배경음악을 실시간 생성합니다</p>
                        </div>

                        <!-- Prompt Input -->
                        <div class="sl-section">
                            <div class="sl-section-title">음악 프롬프트</div>
                            <textarea class="sl-input" x-model="bgmPrompt" rows="3"
                                      placeholder="예: 밝고 희망적인 어쿠스틱 기타와 피아노, 부드러운 드럼"
                                      style="resize: vertical; min-height: 72px;"></textarea>
                        </div>

                        <!-- Options Row -->
                        <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px;">
                            <div>
                                <div class="sl-section-title">BPM</div>
                                <div style="display: flex; align-items: center; gap: 6px;">
                                    <input type="range" class="sl-slider" min="60" max="200" x-model.number="bgmBpm" style="flex:1;">
                                    <span style="font-size: 12px; font-weight: 600; min-width: 32px;" x-text="bgmBpm"></span>
                                </div>
                            </div>
                            <div>
                                <div class="sl-section-title">밀도 (Density)</div>
                                <div style="display: flex; align-items: center; gap: 6px;">
                                    <input type="range" class="sl-slider" min="0" max="100" x-model.number="bgmDensity" style="flex:1;">
                                    <span style="font-size: 12px; font-weight: 600; min-width: 32px;" x-text="(bgmDensity / 100).toFixed(1)"></span>
                                </div>
                            </div>
                            <div>
                                <div class="sl-section-title">밝기 (Brightness)</div>
                                <div style="display: flex; align-items: center; gap: 6px;">
                                    <input type="range" class="sl-slider" min="0" max="100" x-model.number="bgmBrightness" style="flex:1;">
                                    <span style="font-size: 12px; font-weight: 600; min-width: 32px;" x-text="(bgmBrightness / 100).toFixed(1)"></span>
                                </div>
                            </div>
                            <div>
                                <div class="sl-section-title">스케일</div>
                                <select class="sl-select" x-model="bgmScale">
                                    <option value="C_MAJOR_A_MINOR">C Major / A Minor</option>
                                    <option value="D_FLAT_MAJOR_B_FLAT_MINOR">D♭ Major / B♭ Minor</option>
                                    <option value="D_MAJOR_B_MINOR">D Major / B Minor</option>
                                    <option value="E_FLAT_MAJOR_C_MINOR">E♭ Major / C Minor</option>
                                    <option value="E_MAJOR_C_SHARP_MINOR">E Major / C# Minor</option>
                                    <option value="F_MAJOR_D_MINOR">F Major / D Minor</option>
                                    <option value="G_FLAT_MAJOR_E_FLAT_MINOR">G♭ Major / E♭ Minor</option>
                                    <option value="G_MAJOR_E_MINOR">G Major / E Minor</option>
                                    <option value="A_FLAT_MAJOR_F_MINOR">A♭ Major / F Minor</option>
                                    <option value="A_MAJOR_F_SHARP_MINOR">A Major / F# Minor</option>
                                    <option value="B_FLAT_MAJOR_G_MINOR">B♭ Major / G Minor</option>
                                    <option value="B_MAJOR_G_SHARP_MINOR">B Major / G# Minor</option>
                                </select>
                            </div>
                        </div>

                        <!-- Duration & Generation Controls -->
                        <div style="display: flex; gap: 12px; margin-bottom: 16px; align-items: flex-end;">
                            <div style="flex: 1;">
                                <div class="sl-section-title">녹음 길이</div>
                                <div style="display: flex; align-items: center; gap: 8px;">
                                    <input type="range" class="sl-slider" min="3" max="30" x-model.number="bgmDuration" style="flex:1;">
                                    <span style="font-size: 12px; font-weight: 600; min-width: 36px;" x-text="bgmDuration + '초'"></span>
                                </div>
                            </div>
                            <div>
                                <button class="sl-btn primary lg" style="justify-content: center; min-width: 180px;"
                                        @click="generateBgm()" :disabled="bgmLoading || !bgmPrompt.trim()">
                                    <template x-if="!bgmLoading">
                                        <span><i class="ri-music-ai-line"></i> 배경음악 생성</span>
                                    </template>
                                    <template x-if="bgmLoading">
                                        <span><i class="ri-loader-4-line" style="animation: spin 1s linear infinite;"></i>
                                            <span x-text="bgmProgress"></span>
                                        </span>
                                    </template>
                                </button>
                            </div>
                        </div>

                        <!-- BGM Result -->
                        <template x-if="bgmBuffer">
                            <div style="background: var(--sl-card); border: 1px solid var(--sl-green); border-radius: 10px; padding: 16px; margin-bottom: 16px;">
                                <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
                                    <span style="font-size: 20px;">🎼</span>
                                    <div style="flex: 1;">
                                        <div style="font-size: 14px; font-weight: 600;">AI 배경음악 생성 완료</div>
                                        <div style="font-size: 11px; color: var(--sl-text2);">
                                            <span x-text="bgmBuffer.duration.toFixed(1) + '초'"></span> ·
                                            <span x-text="bgmBuffer.numberOfChannels + 'ch'"></span> ·
                                            <span x-text="bgmBuffer.sampleRate + 'Hz'"></span>
                                        </div>
                                    </div>
                                </div>

                                <!-- BGM Volume -->
                                <div class="sl-param" style="margin-bottom: 12px;">
                                    <div class="sl-param-label"><span>배경음악 볼륨</span><span x-text="(bgmVolume * 100).toFixed(0) + '%'"></span></div>
                                    <input type="range" class="sl-slider" min="0" max="100" x-model.number="bgmVolume"
                                           :style="'background: linear-gradient(to right, var(--sl-green) ' + (bgmVolume * 100) + '%, var(--sl-card2) ' + (bgmVolume * 100) + '%);'"
                                           x-on:input="bgmVolume = $event.target.value / 100">
                                </div>

                                <div style="display: flex; gap: 8px;">
                                    <button class="sl-btn play" style="flex: 1; justify-content: center;" @click="playBgmOnly()">
                                        <i class="ri-play-fill"></i> 배경음악만 재생
                                    </button>
                                    <button class="sl-btn success" style="flex: 1; justify-content: center;" @click="exportBgmWav()">
                                        <i class="ri-download-line"></i> WAV 다운로드
                                    </button>
                                    <button class="sl-btn danger sm" @click="clearBgm()">
                                        <i class="ri-delete-bin-line"></i>
                                    </button>
                                </div>
                            </div>
                        </template>

                        <!-- BGM Error -->
                        <template x-if="bgmError">
                            <div style="background: rgba(239,68,68,.1); border: 1px solid var(--sl-red); border-radius: 10px; padding: 12px; margin-bottom: 16px;">
                                <div style="font-size: 12px; color: var(--sl-red);">
                                    <i class="ri-error-warning-line"></i> <span x-text="bgmError"></span>
                                </div>
                            </div>
                        </template>

                        <!-- Quick Prompts for BGM -->
                        <div class="sl-section">
                            <div class="sl-section-title">빠른 프롬프트 예시</div>
                            <div style="display: flex; flex-wrap: wrap; gap: 6px;">
                                <template x-for="qp in bgmQuickPrompts" :key="qp">
                                    <button class="sl-btn sm" @click="bgmPrompt = qp" x-text="qp"></button>
                                </template>
                            </div>
                        </div>

                        <!-- Info box -->
                        <div style="background: rgba(99,102,241,.08); border: 1px solid rgba(99,102,241,.3); border-radius: 10px; padding: 12px; margin-top: 16px;">
                            <div style="font-size: 12px; color: var(--sl-text2); line-height: 1.6;">
                                <strong style="color: var(--sl-text);">💡 합성 가이드</strong><br>
                                • 시퀀서(수동/프리셋/AI생성) + 음성(TTS) + 배경음악(Lyria) 3중 합성 가능<br>
                                • 배경음악 생성 후 <strong>▶ 전체 재생</strong> 버튼으로 합성 결과를 확인하세요<br>
                                • WAV 내보내기 시 모든 레이어가 하나의 파일로 합쳐집니다<br>
                                • Google Lyria RealTime (실험적 모델) — 다중 악기 실시간 생성
                            </div>
                        </div>
                    </div>
                </div>
            </template>

            <!-- Manual Mode -->
            <template x-if="mode === 'manual'">
                <div>
                    <!-- Note Sequencer -->
                    <div class="sl-section">
                        <div class="sl-section-title">음표 시퀀서 — 클릭하여 편집, 바 높이 = 음 높이</div>
                        <div class="sl-sequencer" @click.self="pickerIdx = -1">
                            <template x-for="(n, i) in notes" :key="i">
                                <div class="sl-note" :class="{
                                        'sl-rest': n.type === 'rest',
                                        'sl-chord': n.type === 'chord',
                                        'playing': playingIdx === i,
                                        'active': selectedIdx === i
                                     }"
                                     @click="selectedIdx = i; pickerIdx = (pickerIdx === i ? -1 : i);"
                                     style="position: relative;">
                                    <div class="sl-note-bar"
                                         :style="'height:' + getNoteBarHeight(n) + 'px; width:' + Math.max(36, n.duration * 80) + 'px;'"></div>
                                    <div class="sl-note-label" x-text="getNoteLabel(n)"></div>
                                    <div class="sl-note-dur" x-text="n.duration.toFixed(2) + 's'"></div>
                                    <button class="sl-note-del" @click.stop="removeNote(i)">&times;</button>

                                    <!-- Note Picker -->
                                    <div class="sl-note-picker" x-show="pickerIdx === i" @click.stop x-cloak>
                                        <template x-for="nn in allNotes" :key="nn">
                                            <button :class="nn.includes('#') ? 'black' : ''"
                                                    @click="updateNoteValue(i, nn); pickerIdx = -1;"
                                                    x-text="nn"></button>
                                        </template>
                                        <div style="grid-column: span 7; border-top: 1px solid var(--sl-border); padding-top: 6px; margin-top: 4px;">
                                            <div style="display: flex; gap: 4px; align-items: center;">
                                                <span style="font-size: 10px; color: var(--sl-text2); white-space: nowrap;">길이:</span>
                                                <input type="range" class="sl-slider" min="5" max="200" :value="notes[i]?.duration * 100"
                                                       @input="notes[i].duration = $event.target.value / 100" style="flex:1;">
                                                <span style="font-size: 10px; color: var(--sl-text); min-width: 32px;" x-text="notes[i]?.duration.toFixed(2) + 's'"></span>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </template>

                            <!-- Add buttons -->
                            <div style="display: flex; flex-direction: column; gap: 4px;">
                                <div class="sl-add-note" @click="addNote('note')" style="min-height: 50px;" title="음표 추가">
                                    <i class="ri-add-line"></i>
                                </div>
                                <div class="sl-add-note" @click="addNote('rest')" style="min-height: 30px; font-size: 12px;" title="쉼표 추가">
                                    <i class="ri-space"></i>
                                </div>
                            </div>
                        </div>
                    </div>

                    <!-- Waveform -->
                    <div class="sl-section">
                        <div class="sl-section-title">파형</div>
                        <canvas class="sl-waveform" x-ref="waveformCanvas"></canvas>
                    </div>
                </div>
            </template>
        </div>
    </div>

    <!-- Transport Bar -->
    <div class="sl-transport">
        <button class="sl-btn play lg" @click="playAll()" :disabled="isPlaying">
            <i class="ri-play-fill"></i> 재생
        </button>
        <button class="sl-btn stop" @click="stopAll()" :disabled="!isPlaying">
            <i class="ri-stop-fill"></i>
        </button>
        <div style="flex: 1;"></div>
        <button class="sl-btn" @click="exportWav()">
            <i class="ri-download-line"></i> WAV 저장
        </button>
        <button class="sl-btn" @click="exportJson()">
            <i class="ri-file-code-line"></i> JSON
        </button>
        <button class="sl-btn" @click="importJson()">
            <i class="ri-upload-line"></i> 불러오기
        </button>
    </div>

    <!-- Help Modal -->
    <template x-if="showHelp">
        <div class="sl-modal-overlay" @click.self="showHelp = false" @keydown.escape.window="showHelp = false">
            <div class="sl-modal">
                <div class="sl-modal-header">
                    <i class="ri-book-open-line" style="font-size: 20px; color: var(--sl-indigo);"></i>
                    <h2>사운드 로고 생성기 사용법</h2>
                    <button class="sl-btn sm" @click="showHelp = false" style="border-radius: 50%; width: 28px; height: 28px; padding: 0; justify-content: center;">
                        <i class="ri-close-line"></i>
                    </button>
                </div>
                <div class="sl-modal-body">

                    <h3><i class="ri-information-line" style="color: var(--sl-indigo);"></i> 개요</h3>
                    <p>사운드 로고 생성기는 <strong>브랜드 시그니처 사운드</strong>를 만드는 도구입니다.<br>
                    3개의 레이어를 조합하여 하나의 완성된 사운드 로고를 제작할 수 있습니다.</p>

                    <table class="help-table">
                        <tr>
                            <th><i class="ri-piano-line"></i> 시퀀서</th>
                            <td>음표를 배치하여 멜로디/효과음 제작 (Web Audio API 신스사이저)</td>
                        </tr>
                        <tr>
                            <th><i class="ri-mic-line"></i> 음성 (TTS)</th>
                            <td>"쌤!", "SAM" 등 음성 오버레이 추가 (Google Gemini TTS)</td>
                        </tr>
                        <tr>
                            <th><i class="ri-music-ai-line"></i> 배경음악</th>
                            <td>다중 악기 배경음악 생성 (Google Lyria RealTime AI)</td>
                        </tr>
                    </table>

                    <hr class="help-divider">

                    <h3><i class="ri-piano-line" style="color: var(--sl-blue);"></i> 탭 1 — 수동 모드</h3>
                    <p>음표를 하나씩 배치하여 직접 사운드를 디자인합니다.</p>

                    <div class="help-step">
                        <div class="help-step-num">1</div>
                        <div class="help-step-text"><strong>좌측 사이드바</strong>에서 음색(Sine/Square/Triangle/Sawtooth)을 선택합니다.</div>
                    </div>
                    <div class="help-step">
                        <div class="help-step-num">2</div>
                        <div class="help-step-text">시퀀서 영역의 <strong>+ 버튼</strong>을 클릭하여 음표/화음/쉼표를 추가합니다.</div>
                    </div>
                    <div class="help-step">
                        <div class="help-step-num">3</div>
                        <div class="help-step-text">음표를 클릭하면 <strong>음 높이 선택 피커</strong>가 열립니다. 음표 위 <strong>x 버튼</strong>으로 삭제합니다.</div>
                    </div>
                    <div class="help-step">
                        <div class="help-step-num">4</div>
                        <div class="help-step-text">하단 파라미터(ADSR, 볼륨, 리버브)로 음색을 미세 조정합니다.</div>
                    </div>

                    <table class="help-table">
                        <tr><th>Attack</th><td>소리가 최대 볼륨에 도달하는 시간 (ms)</td></tr>
                        <tr><th>Decay</th><td>최대 볼륨에서 지속 레벨로 감쇄하는 시간 (ms)</td></tr>
                        <tr><th>Sustain</th><td>음이 유지되는 동안의 볼륨 비율 (0~1)</td></tr>
                        <tr><th>Release</th><td>건반에서 손을 뗀 후 소리가 사라지는 시간 (ms)</td></tr>
                    </table>

                    <hr class="help-divider">

                    <h3><i class="ri-magic-line" style="color: var(--sl-purple);"></i> 탭 2 — 프리셋</h3>
                    <p><strong>50종의 프리셋 템플릿</strong>을 8개 카테고리에서 선택할 수 있습니다.</p>

                    <table class="help-table">
                        <tr><th>기업 시그널</th><td>스타트업 로고, 이메일 송신 등</td></tr>
                        <tr><th>알림/메시지</th><td>카카오톡, 알람 등 알림음</td></tr>
                        <tr><th>상태/피드백</th><td>성공, 에러, 경고 효과음</td></tr>
                        <tr><th>전환 효과</th><td>화면 전환, 팝업 등</td></tr>
                        <tr><th>게임 효과</th><td>코인, 레벨업, 게임오버</td></tr>
                        <tr><th>UI 인터랙션</th><td>클릭, 호버, 토글</td></tr>
                        <tr><th>브랜드 징글</th><td>광고, 라디오 CM 징글</td></tr>
                        <tr><th>방송/미디어</th><td>뉴스 속보, 시보 등</td></tr>
                    </table>
                    <p>프리셋을 클릭하면 시퀀서에 자동 로드되며, <strong>수동 모드에서 추가 편집</strong>이 가능합니다.</p>

                    <hr class="help-divider">

                    <h3><i class="ri-sparkling-line" style="color: var(--sl-amber);"></i> 탭 3 — AI 생성</h3>
                    <p>텍스트 프롬프트를 입력하면 <strong>Gemini AI가 음표 시퀀스를 자동 설계</strong>합니다.</p>

                    <div class="help-step">
                        <div class="help-step-num">1</div>
                        <div class="help-step-text">원하는 사운드를 자연어로 설명합니다. (예: "밝고 미래적인 IT 기업 로고")</div>
                    </div>
                    <div class="help-step">
                        <div class="help-step-num">2</div>
                        <div class="help-step-text">카테고리와 목표 길이를 설정하고 <strong>"AI로 생성하기"</strong>를 클릭합니다.</div>
                    </div>
                    <div class="help-step">
                        <div class="help-step-num">3</div>
                        <div class="help-step-text">결과 미리보기에서 <strong>미리듣기</strong> 후, 마음에 들면 <strong>"시퀀서에 로드"</strong>합니다.</div>
                    </div>
                    <p><strong>빠른 프롬프트</strong> 버튼을 클릭하면 예시 문장이 자동 입력됩니다.</p>

                    <hr class="help-divider">

                    <h3><i class="ri-music-ai-line" style="color: var(--sl-green);"></i> 탭 4 — AI 배경음악 (Lyria)</h3>
                    <p>Google Lyria RealTime AI로 <strong>다중 악기 배경음악</strong>을 실시간 생성합니다.</p>

                    <div class="help-step">
                        <div class="help-step-num">1</div>
                        <div class="help-step-text">음악 프롬프트를 입력합니다. (예: "밝고 희망적인 어쿠스틱 기타와 피아노")</div>
                    </div>
                    <div class="help-step">
                        <div class="help-step-num">2</div>
                        <div class="help-step-text">BPM, 밀도, 밝기, 스케일(조)을 조정합니다.</div>
                    </div>
                    <div class="help-step">
                        <div class="help-step-num">3</div>
                        <div class="help-step-text">녹음 길이(3~30초)를 설정하고 <strong>"배경음악 생성"</strong>을 클릭합니다.</div>
                    </div>
                    <div class="help-step">
                        <div class="help-step-num">4</div>
                        <div class="help-step-text">생성 완료 후 <strong>볼륨을 조절</strong>하고 시퀀서/음성과 합성합니다.</div>
                    </div>

                    <table class="help-table">
                        <tr><th>BPM</th><td>분당 비트 수 (60=느림, 120=보통, 200=빠름)</td></tr>
                        <tr><th>밀도</th><td>악기/음의 밀집도 (0=sparse, 1=dense)</td></tr>
                        <tr><th>밝기</th><td>음색의 밝기 (0=어두움, 1=밝음)</td></tr>
                        <tr><th>스케일</th><td>음악의 조성 (예: C Major, A Minor)</td></tr>
                    </table>

                    <hr class="help-divider">

                    <h3><i class="ri-mic-line" style="color: var(--sl-green);"></i> 음성 오버레이 (TTS)</h3>
                    <p>좌측 사이드바 하단의 <strong>음성 오버레이</strong> 영역에서 텍스트를 입력하고 음성을 생성합니다.</p>

                    <div class="help-step">
                        <div class="help-step-num">1</div>
                        <div class="help-step-text">텍스트 입력 (예: "쌤!", "SAM", "코드브릿지엑스").</div>
                    </div>
                    <div class="help-step">
                        <div class="help-step-num">2</div>
                        <div class="help-step-text"><strong>"음성 생성"</strong> 클릭 → AI가 자연스러운 한국어 음성을 생성합니다.</div>
                    </div>
                    <div class="help-step">
                        <div class="help-step-num">3</div>
                        <div class="help-step-text"><strong>시작 시점</strong>(지연 시간)과 <strong>볼륨</strong>을 조절합니다.</div>
                    </div>

                    <hr class="help-divider">

                    <h3><i class="ri-play-circle-line" style="color: var(--sl-green);"></i> 재생 및 내보내기</h3>

                    <table class="help-table">
                        <tr>
                            <th style="white-space: nowrap;"><span class="help-badge" style="background: var(--sl-green); color: #fff;">▶ 전체 재생</span></th>
                            <td>시퀀서 + 음성 + 배경음악을 <strong>동시에 합성 재생</strong>합니다.</td>
                        </tr>
                        <tr>
                            <th style="white-space: nowrap;"><span class="help-badge" style="background: var(--sl-red); color: #fff;">■ 정지</span></th>
                            <td>재생을 즉시 멈춥니다.</td>
                        </tr>
                        <tr>
                            <th style="white-space: nowrap;"><span class="help-badge" style="background: var(--sl-card2); color: var(--sl-text);">WAV</span></th>
                            <td>3개 레이어가 합쳐진 <strong>WAV 파일을 다운로드</strong>합니다.</td>
                        </tr>
                        <tr>
                            <th style="white-space: nowrap;"><span class="help-badge" style="background: var(--sl-card2); color: var(--sl-text);">JSON</span></th>
                            <td>시퀀서 설정을 JSON으로 <strong>내보내기/불러오기</strong> 합니다.</td>
                        </tr>
                        <tr>
                            <th style="white-space: nowrap;"><span class="help-badge" style="background: var(--sl-card2); color: var(--sl-text);">저장</span></th>
                            <td>현재 시퀀서 설정을 브라우저 로컬 저장소에 저장합니다.</td>
                        </tr>
                    </table>

                    <hr class="help-divider">

                    <h3><i class="ri-stack-line" style="color: var(--sl-indigo);"></i> 3중 합성 구조</h3>
                    <p>최종 사운드 로고는 아래 3개의 레이어가 믹싱되어 출력됩니다.</p>

                    <div style="background: var(--sl-bg); border-radius: 8px; padding: 14px; font-family: monospace; font-size: 12px; line-height: 1.8; margin: 8px 0;">
                        <span style="color: var(--sl-blue);">Layer 1</span> 시퀀서 (수동/프리셋/AI) ──┐<br>
                        <span style="color: var(--sl-green);">Layer 2</span> 음성 TTS (Gemini) ────┼──→ <strong style="color: var(--sl-text);">합성 재생 / WAV 내보내기</strong><br>
                        <span style="color: var(--sl-amber);">Layer 3</span> 배경음악 (Lyria) ────┘
                    </div>

                    <p style="margin-top: 12px; font-size: 11px; color: var(--sl-text2);">
                        각 레이어는 독립적으로 사용 가능합니다. 시퀀서만, 음성만, 배경음악만 사용해도 됩니다.
                    </p>

                </div>
            </div>
        </div>
    </template>

    <!-- Toast -->
    <div class="sl-toast" :class="toastType" x-show="toastMsg" x-text="toastMsg"
         x-transition.opacity.duration.300ms x-cloak></div>
</div>

@endsection

@push('scripts')
<script>
function soundLogo() {
    return {
        mode: 'manual',
        showHelp: false,
        synth: 'sine',
        adsr: { attack: 10, decay: 100, sustain: 0.7, release: 500 },
        volume: 0.8,
        reverb: 0.2,
        notes: [],
        selectedIdx: -1,
        pickerIdx: -1,
        playingIdx: -1,
        isPlaying: false,
        toastMsg: '',
        toastType: 'info',
        savedSounds: [],
        currentSoundId: null,
        presetIdx: -1,
        presetCategory: 'all',
        audioCtx: null,

        // AI 어시스트
        aiPrompt: '',
        aiCategory: '기업 시그널',
        aiDurationRaw: 15,
        aiLoading: false,
        aiResult: null,
        aiError: '',
        // 배경음악 (Lyria RealTime)
        bgmPrompt: '',
        bgmBpm: 120,
        bgmDensity: 50,
        bgmBrightness: 50,
        bgmScale: 'C_MAJOR_A_MINOR',
        bgmDuration: 10,
        bgmLoading: false,
        bgmProgress: '연결 중...',
        bgmBuffer: null,
        bgmVolume: 0.5,
        bgmError: '',
        bgmWs: null,
        bgmQuickPrompts: [
            '밝고 희망적인 어쿠스틱 기타와 피아노',
            '세련된 재즈 피아노 트리오',
            '에너지 넘치는 일렉트로닉 팝',
            '차분한 로파이 힙합 비트',
            '웅장한 오케스트라 팡파르',
            '부드러운 보사노바 기타',
            '미니멀 테크노, 깊은 베이스',
            '몽환적인 앰비언트 신스패드',
            '펑키한 슬랩 베이스와 브라스',
            '클래식 록 기타 리프와 드럼',
        ],
        // 음성 오버레이
        voiceText: '우리들의 솔루션 ~ 쌤, 쌤, 쌤',
        voiceAudioData: null,
        voiceMimeType: '',
        voiceLoading: false,
        voiceDelay: 0.0,
        voiceVolume: 0.8,
        voiceBuffer: null,
        voiceCategory: 'female',
        voiceName: 'Kore',
        voiceStyle: '',
        voiceSpeed: 3,
        voiceGroups: {
            female: [
                { value: 'Kore', label: '코어 — 단정하고 명확한' },
                { value: 'Aoede', label: '아오이데 — 산뜻하고 가벼운' },
                { value: 'Leda', label: '레다 — 젊고 발랄한' },
                { value: 'Zephyr', label: '제피르 — 밝고 경쾌한' },
                { value: 'Achernar', label: '아케르나르 — 부드럽고 섬세한' },
                { value: 'Sulafat', label: '술라파트 — 따뜻하고 포근한' },
                { value: 'Gacrux', label: '가크룩스 — 성숙하고 중후한' },
                { value: 'Vindemiatrix', label: '빈데미아트릭스 — 차분한' },
                { value: 'Pulcherrima', label: '풀체리마 — 또렷하고 앞선' },
            ],
            male: [
                { value: 'Puck', label: '퍽 — 밝고 활발한' },
                { value: 'Charon', label: '카론 — 안정적이고 신뢰감' },
                { value: 'Fenrir', label: '펜리르 — 에너지 넘치는' },
                { value: 'Orus', label: '오루스 — 무게감 있는' },
                { value: 'Achird', label: '아키르드 — 친근하고 다정한' },
                { value: 'Algieba', label: '알기에바 — 매끄럽고 세련된' },
                { value: 'Schedar', label: '쉐다르 — 균일하고 안정적' },
                { value: 'Alnilam', label: '알닐람 — 단단하고 힘 있는' },
                { value: 'Sadaltager', label: '사달타게르 — 지적이고 차분한' },
            ],
            child: [
                { value: 'Leda', label: '여자아이 (레다 기반) — 발랄한' },
                { value: 'Zephyr', label: '여자아이 (제피르 기반) — 경쾌한' },
                { value: 'Achernar', label: '여자아이 (아케르나르 기반) — 섬세한' },
                { value: 'Puck', label: '남자아이 (퍽 기반) — 활발한' },
                { value: 'Fenrir', label: '남자아이 (펜리르 기반) — 에너지' },
            ],
        },
        voiceStyleOptions: [
            { value: '', label: '기본 톤' },
            { value: '밝고 활기차게 말해주세요', label: '밝고 활기차게' },
            { value: '차분하고 신뢰감 있게 말해주세요', label: '차분하고 신뢰감 있게' },
            { value: '따뜻하고 부드럽게 말해주세요', label: '따뜻하고 부드럽게' },
            { value: '힘 있고 당당하게 말해주세요', label: '힘 있고 당당하게' },
            { value: '귀엽고 사랑스럽게 말해주세요', label: '귀엽고 사랑스럽게' },
            { value: '진지하고 전문적으로 말해주세요', label: '진지하고 전문적으로' },
            { value: '속삭이듯 조용히 말해주세요', label: '속삭이듯 조용히' },
            { value: '흥분하고 신나게 말해주세요', label: '흥분하고 신나게' },
        ],
        voiceSpeedLabels: ['매우 느리게', '느리게', '보통', '빠르게', '매우 빠르게'],

        aiQuickPrompts: [
            '밝고 미래적인 IT 기업 로고',
            '따뜻하고 친근한 카페 알림음',
            '긴박한 뉴스 속보 시그널',
            '귀여운 모바일 앱 알림',
            '웅장한 영화 오프닝 사운드',
            '8bit 레트로 게임 코인 효과',
            '세련된 재즈 브랜드 징글',
            '차분한 명상 앱 시작음',
            '에너지 넘치는 유튜브 인트로',
            '고급스러운 호텔 안내 벨',
        ],

        synthTypes: [
            { code: 'sine',     label: 'Sine (부드러움)',      icon: '🔵' },
            { code: 'square',   label: 'Square (8bit)',        icon: '🟪' },
            { code: 'triangle', label: 'Triangle (따뜻함)',    icon: '🔺' },
            { code: 'sawtooth', label: 'Sawtooth (날카로움)',   icon: '🟧' },
        ],

        presetCategories: [
            { code: 'all', label: '전체' },
            { code: 'corporate', label: '기업 시그널' },
            { code: 'notification', label: '알림/메시지' },
            { code: 'status', label: '상태/피드백' },
            { code: 'transition', label: '전환 효과' },
            { code: 'game', label: '게임 효과' },
            { code: 'ui', label: 'UI 인터랙션' },
            { code: 'jingle', label: '브랜드 징글' },
            { code: 'broadcast', label: '방송/미디어' },
        ],

        allNotes: [
            'C3','D3','E3','F3','G3','A3','B3',
            'C4','D4','E4','F4','G4','A4','B4',
            'C5','D5','E5','F5','G5','A5','B5','C6',
        ],

        noteFreq: {
            'C3':130.81,'C#3':138.59,'D3':146.83,'D#3':155.56,'E3':164.81,'F3':174.61,
            'F#3':185.00,'G3':196.00,'G#3':207.65,'A3':220.00,'A#3':233.08,'B3':246.94,
            'C4':261.63,'C#4':277.18,'D4':293.66,'D#4':311.13,'E4':329.63,'F4':349.23,
            'F#4':369.99,'G4':392.00,'G#4':415.30,'A4':440.00,'A#4':466.16,'B4':493.88,
            'C5':523.25,'C#5':554.37,'D5':587.33,'D#5':622.25,'E5':659.25,'F5':698.46,
            'F#5':739.99,'G5':783.99,'G#5':830.61,'A5':880.00,'A#5':932.33,'B5':987.77,
            'C6':1046.50
        },

        presets: [
            // ━━━ 기업 시그널 (Corporate) ━━━
            { cat:'corporate', name:'기업 시그널 (밝음)', icon:'🏢', style:'Intel 스타일', duration:'1.5', desc:'밝고 미래적인 5음 시그널',
              synth:'sine', adsr:{attack:10,decay:80,sustain:0.6,release:400}, volume:0.8, reverb:0.3,
              notes:[{type:'note',note:'G5',duration:0.18,velocity:0.8},{type:'note',note:'D5',duration:0.18,velocity:0.9},{type:'note',note:'G5',duration:0.18,velocity:0.7},{type:'note',note:'D5',duration:0.18,velocity:0.8},{type:'note',note:'G5',duration:0.50,velocity:1.0}] },
            { cat:'corporate', name:'기업 시그널 (무게감)', icon:'🎬', style:'Netflix 스타일', duration:'2.5', desc:'깊은 울림의 코드 시그널',
              synth:'sine', adsr:{attack:30,decay:200,sustain:0.8,release:2000}, volume:0.9, reverb:0.6,
              notes:[{type:'chord',chord:['D3','A3'],duration:0.6,velocity:0.9},{type:'rest',duration:0.15},{type:'chord',chord:['D3','A3','D4','F#4'],duration:1.2,velocity:1.0}] },
            { cat:'corporate', name:'기업 시그널 (미래)', icon:'🚀', style:'Microsoft 스타일', duration:'2.0', desc:'상승 4음 테크 시그널',
              synth:'sine', adsr:{attack:20,decay:100,sustain:0.7,release:600}, volume:0.8, reverb:0.4,
              notes:[{type:'note',note:'E4',duration:0.25,velocity:0.6},{type:'note',note:'B4',duration:0.25,velocity:0.7},{type:'note',note:'D#5',duration:0.25,velocity:0.8},{type:'note',note:'E5',duration:0.60,velocity:1.0}] },
            { cat:'corporate', name:'기업 시그널 (단순)', icon:'🍎', style:'Apple 스타일', duration:'1.0', desc:'미니멀 단음 + 여운',
              synth:'sine', adsr:{attack:50,decay:150,sustain:0.8,release:1500}, volume:0.7, reverb:0.5,
              notes:[{type:'note',note:'C5',duration:0.80,velocity:0.9}] },
            { cat:'corporate', name:'기업 시그널 (반복)', icon:'📱', style:'Samsung 스타일', duration:'1.0', desc:'경쾌한 3음 반복 패턴',
              synth:'triangle', adsr:{attack:5,decay:50,sustain:0.5,release:200}, volume:0.7, reverb:0.2,
              notes:[{type:'note',note:'E5',duration:0.12,velocity:0.7},{type:'note',note:'E5',duration:0.12,velocity:0.8},{type:'rest',duration:0.05},{type:'note',note:'G5',duration:0.12,velocity:0.9},{type:'note',note:'E5',duration:0.30,velocity:1.0}] },
            { cat:'corporate', name:'기업 시그널 (디지털)', icon:'💻', style:'스타트업', duration:'1.5', desc:'디지털 아르페지오 상승',
              synth:'square', adsr:{attack:5,decay:40,sustain:0.4,release:300}, volume:0.5, reverb:0.2,
              notes:[{type:'note',note:'C4',duration:0.10,velocity:0.5},{type:'note',note:'E4',duration:0.10,velocity:0.6},{type:'note',note:'G4',duration:0.10,velocity:0.7},{type:'note',note:'C5',duration:0.10,velocity:0.8},{type:'note',note:'E5',duration:0.40,velocity:1.0}] },
            { cat:'corporate', name:'기업 시그널 (프리미엄)', icon:'👑', style:'럭셔리', duration:'3.0', desc:'깊은 베이스 + 하이 코드',
              synth:'sine', adsr:{attack:40,decay:200,sustain:0.8,release:2500}, volume:0.9, reverb:0.7,
              notes:[{type:'note',note:'F3',duration:0.8,velocity:0.7},{type:'rest',duration:0.2},{type:'chord',chord:['C4','E4','G4','C5'],duration:1.5,velocity:1.0}] },

            // ━━━ 알림/메시지 (Notification) ━━━
            { cat:'notification', name:'알림음 (경쾌)', icon:'💬', style:'카카오톡 스타일', duration:'0.5', desc:'짧고 귀여운 2음 알림',
              synth:'triangle', adsr:{attack:5,decay:50,sustain:0.4,release:200}, volume:0.7, reverb:0.1,
              notes:[{type:'note',note:'E5',duration:0.12,velocity:0.8},{type:'note',note:'A5',duration:0.20,velocity:0.9}] },
            { cat:'notification', name:'알림음 (정보)', icon:'🔔', style:'Slack 스타일', duration:'1.0', desc:'차분한 3음 정보 알림',
              synth:'sine', adsr:{attack:10,decay:80,sustain:0.5,release:300}, volume:0.6, reverb:0.2,
              notes:[{type:'note',note:'C5',duration:0.15,velocity:0.7},{type:'note',note:'E5',duration:0.15,velocity:0.8},{type:'note',note:'G5',duration:0.30,velocity:0.9}] },
            { cat:'notification', name:'알림음 (부드러움)', icon:'🕊️', style:'iMessage 스타일', duration:'0.8', desc:'부드러운 상승 3음',
              synth:'sine', adsr:{attack:15,decay:60,sustain:0.6,release:400}, volume:0.6, reverb:0.3,
              notes:[{type:'note',note:'F5',duration:0.12,velocity:0.6},{type:'note',note:'A5',duration:0.12,velocity:0.7},{type:'note',note:'C6',duration:0.30,velocity:0.8}] },
            { cat:'notification', name:'알림음 (채팅)', icon:'💭', style:'Discord 스타일', duration:'0.4', desc:'빠른 2음 팝 사운드',
              synth:'triangle', adsr:{attack:3,decay:30,sustain:0.3,release:150}, volume:0.6, reverb:0.1,
              notes:[{type:'note',note:'E4',duration:0.08,velocity:0.7},{type:'note',note:'G4',duration:0.15,velocity:0.8}] },
            { cat:'notification', name:'알림음 (이메일)', icon:'📧', style:'전통 메일', duration:'1.2', desc:'클래식 메일 도착음',
              synth:'sine', adsr:{attack:10,decay:100,sustain:0.5,release:500}, volume:0.6, reverb:0.3,
              notes:[{type:'note',note:'E5',duration:0.15,velocity:0.7},{type:'rest',duration:0.05},{type:'note',note:'E5',duration:0.12,velocity:0.8},{type:'note',note:'G5',duration:0.25,velocity:0.7},{type:'note',note:'E5',duration:0.35,velocity:0.6}] },
            { cat:'notification', name:'알림음 (긴급)', icon:'🚨', style:'긴급 알림', duration:'0.8', desc:'빠른 반복 긴급 알림',
              synth:'square', adsr:{attack:3,decay:20,sustain:0.5,release:80}, volume:0.7, reverb:0.1,
              notes:[{type:'note',note:'A5',duration:0.08,velocity:0.9},{type:'rest',duration:0.03},{type:'note',note:'A5',duration:0.08,velocity:1.0},{type:'rest',duration:0.03},{type:'note',note:'A5',duration:0.08,velocity:1.0},{type:'rest',duration:0.03},{type:'note',note:'A5',duration:0.15,velocity:1.0}] },
            { cat:'notification', name:'알림음 (리마인더)', icon:'⏰', style:'차임벨', duration:'1.5', desc:'은은한 차임 리마인더',
              synth:'sine', adsr:{attack:20,decay:120,sustain:0.5,release:800}, volume:0.5, reverb:0.5,
              notes:[{type:'note',note:'G5',duration:0.25,velocity:0.7},{type:'note',note:'E5',duration:0.25,velocity:0.6},{type:'note',note:'C5',duration:0.25,velocity:0.5},{type:'note',note:'G5',duration:0.50,velocity:0.8}] },

            // ━━━ 상태/피드백 (Status) ━━━
            { cat:'status', name:'성공 사운드', icon:'✅', style:'레벨업', duration:'1.0', desc:'상승 진행 4음 성공 효과',
              synth:'triangle', adsr:{attack:5,decay:60,sustain:0.5,release:400}, volume:0.7, reverb:0.3,
              notes:[{type:'note',note:'C5',duration:0.12,velocity:0.7},{type:'note',note:'E5',duration:0.12,velocity:0.8},{type:'note',note:'G5',duration:0.12,velocity:0.9},{type:'note',note:'C6',duration:0.40,velocity:1.0}] },
            { cat:'status', name:'에러 사운드', icon:'❌', style:'경고음', duration:'0.5', desc:'낮은 2음 하강 경고',
              synth:'square', adsr:{attack:5,decay:40,sustain:0.3,release:150}, volume:0.5, reverb:0.1,
              notes:[{type:'note',note:'E4',duration:0.15,velocity:0.8},{type:'note',note:'C4',duration:0.25,velocity:0.9}] },
            { cat:'status', name:'경고음', icon:'⚠️', style:'주의', duration:'1.0', desc:'반복되는 주의 경고',
              synth:'sawtooth', adsr:{attack:5,decay:30,sustain:0.4,release:100}, volume:0.4, reverb:0.1,
              notes:[{type:'note',note:'A4',duration:0.12,velocity:0.8},{type:'rest',duration:0.08},{type:'note',note:'A4',duration:0.12,velocity:0.9},{type:'rest',duration:0.08},{type:'note',note:'A4',duration:0.30,velocity:1.0}] },
            { cat:'status', name:'완료 사운드', icon:'🎯', style:'태스크 완료', duration:'0.8', desc:'깔끔한 완료 확인음',
              synth:'sine', adsr:{attack:5,decay:50,sustain:0.5,release:300}, volume:0.6, reverb:0.2,
              notes:[{type:'note',note:'G4',duration:0.10,velocity:0.6},{type:'note',note:'D5',duration:0.10,velocity:0.7},{type:'note',note:'G5',duration:0.30,velocity:0.9}] },
            { cat:'status', name:'취소 사운드', icon:'↩️', style:'되돌리기', duration:'0.5', desc:'부드러운 하강 취소음',
              synth:'sine', adsr:{attack:10,decay:60,sustain:0.4,release:200}, volume:0.5, reverb:0.2,
              notes:[{type:'note',note:'B4',duration:0.12,velocity:0.6},{type:'note',note:'G4',duration:0.20,velocity:0.5}] },
            { cat:'status', name:'연결 성공', icon:'🔗', style:'커넥트', duration:'0.8', desc:'상승 2음 연결 확인',
              synth:'sine', adsr:{attack:10,decay:80,sustain:0.6,release:350}, volume:0.6, reverb:0.3,
              notes:[{type:'note',note:'D5',duration:0.15,velocity:0.7},{type:'note',note:'A5',duration:0.35,velocity:0.9}] },
            { cat:'status', name:'연결 해제', icon:'🔌', style:'디스커넥트', duration:'0.8', desc:'하강 2음 연결 해제',
              synth:'sine', adsr:{attack:10,decay:80,sustain:0.5,release:350}, volume:0.5, reverb:0.3,
              notes:[{type:'note',note:'A5',duration:0.15,velocity:0.7},{type:'note',note:'D5',duration:0.35,velocity:0.5}] },

            // ━━━ 전환 효과 (Transition) ━━━
            { cat:'transition', name:'전환 효과 (업)', icon:'⬆️', style:'상승 스윕', duration:'0.5', desc:'빠른 상승 전환음',
              synth:'sine', adsr:{attack:5,decay:30,sustain:0.3,release:100}, volume:0.5, reverb:0.2,
              notes:[{type:'note',note:'C4',duration:0.06,velocity:0.5},{type:'note',note:'E4',duration:0.06,velocity:0.6},{type:'note',note:'G4',duration:0.06,velocity:0.7},{type:'note',note:'C5',duration:0.06,velocity:0.8},{type:'note',note:'E5',duration:0.06,velocity:0.9},{type:'note',note:'G5',duration:0.10,velocity:1.0}] },
            { cat:'transition', name:'전환 효과 (다운)', icon:'⬇️', style:'하강 스윕', duration:'0.5', desc:'빠른 하강 전환음',
              synth:'sine', adsr:{attack:5,decay:30,sustain:0.3,release:100}, volume:0.5, reverb:0.2,
              notes:[{type:'note',note:'G5',duration:0.06,velocity:1.0},{type:'note',note:'E5',duration:0.06,velocity:0.9},{type:'note',note:'C5',duration:0.06,velocity:0.8},{type:'note',note:'G4',duration:0.06,velocity:0.7},{type:'note',note:'E4',duration:0.06,velocity:0.6},{type:'note',note:'C4',duration:0.10,velocity:0.5}] },
            { cat:'transition', name:'전환 (스파크)', icon:'✨', style:'반짝임', duration:'0.6', desc:'높은 음역의 반짝이는 전환',
              synth:'sine', adsr:{attack:3,decay:20,sustain:0.2,release:150}, volume:0.4, reverb:0.4,
              notes:[{type:'note',note:'C6',duration:0.05,velocity:0.9},{type:'note',note:'G5',duration:0.05,velocity:0.7},{type:'note',note:'C6',duration:0.05,velocity:1.0},{type:'note',note:'E5',duration:0.05,velocity:0.6},{type:'note',note:'G5',duration:0.05,velocity:0.8},{type:'note',note:'C6',duration:0.15,velocity:1.0}] },
            { cat:'transition', name:'전환 (워프)', icon:'🌀', style:'워프/이동', duration:'0.8', desc:'공간 이동 느낌의 전환',
              synth:'sawtooth', adsr:{attack:5,decay:50,sustain:0.3,release:200}, volume:0.3, reverb:0.5,
              notes:[{type:'note',note:'C3',duration:0.08,velocity:0.4},{type:'note',note:'G3',duration:0.08,velocity:0.5},{type:'note',note:'E4',duration:0.08,velocity:0.6},{type:'note',note:'C5',duration:0.08,velocity:0.7},{type:'note',note:'G5',duration:0.08,velocity:0.8},{type:'note',note:'C6',duration:0.20,velocity:0.9}] },
            { cat:'transition', name:'전환 (클릭)', icon:'🔘', style:'기계적', duration:'0.2', desc:'짧은 기계적 클릭 전환',
              synth:'square', adsr:{attack:1,decay:10,sustain:0.2,release:50}, volume:0.4, reverb:0.0,
              notes:[{type:'note',note:'C5',duration:0.03,velocity:0.9},{type:'rest',duration:0.02},{type:'note',note:'G5',duration:0.05,velocity:0.7}] },
            { cat:'transition', name:'전환 (슬라이드)', icon:'➡️', style:'글라이드', duration:'0.6', desc:'부드러운 슬라이드 전환',
              synth:'sine', adsr:{attack:20,decay:40,sustain:0.5,release:200}, volume:0.5, reverb:0.3,
              notes:[{type:'note',note:'E4',duration:0.08,velocity:0.5},{type:'note',note:'F4',duration:0.08,velocity:0.5},{type:'note',note:'G4',duration:0.08,velocity:0.6},{type:'note',note:'A4',duration:0.08,velocity:0.6},{type:'note',note:'B4',duration:0.08,velocity:0.7},{type:'note',note:'C5',duration:0.15,velocity:0.8}] },

            // ━━━ 게임 효과 (Game) ━━━
            { cat:'game', name:'코인 획득', icon:'🪙', style:'마리오 스타일', duration:'0.4', desc:'경쾌한 코인 수집 효과',
              synth:'square', adsr:{attack:2,decay:20,sustain:0.3,release:100}, volume:0.5, reverb:0.1,
              notes:[{type:'note',note:'E5',duration:0.08,velocity:0.8},{type:'note',note:'B5',duration:0.20,velocity:1.0}] },
            { cat:'game', name:'파워업', icon:'⚡', style:'버프', duration:'1.0', desc:'상승 아르페지오 파워업',
              synth:'square', adsr:{attack:3,decay:30,sustain:0.4,release:150}, volume:0.5, reverb:0.2,
              notes:[{type:'note',note:'C4',duration:0.06,velocity:0.5},{type:'note',note:'E4',duration:0.06,velocity:0.6},{type:'note',note:'G4',duration:0.06,velocity:0.7},{type:'note',note:'C5',duration:0.06,velocity:0.8},{type:'note',note:'E5',duration:0.06,velocity:0.9},{type:'note',note:'G5',duration:0.06,velocity:1.0},{type:'note',note:'C6',duration:0.30,velocity:1.0}] },
            { cat:'game', name:'게임 오버', icon:'💀', style:'패배', duration:'2.0', desc:'하강 반음계 게임 오버',
              synth:'sawtooth', adsr:{attack:10,decay:100,sustain:0.5,release:800}, volume:0.5, reverb:0.4,
              notes:[{type:'note',note:'E4',duration:0.20,velocity:0.8},{type:'note',note:'D#4',duration:0.20,velocity:0.7},{type:'note',note:'D4',duration:0.20,velocity:0.6},{type:'note',note:'C#4',duration:0.20,velocity:0.5},{type:'note',note:'C4',duration:0.60,velocity:0.4}] },
            { cat:'game', name:'보너스', icon:'🎁', style:'보상', duration:'0.8', desc:'빠른 팡파레 보너스',
              synth:'triangle', adsr:{attack:3,decay:40,sustain:0.4,release:200}, volume:0.6, reverb:0.2,
              notes:[{type:'note',note:'C5',duration:0.06,velocity:0.7},{type:'note',note:'E5',duration:0.06,velocity:0.8},{type:'note',note:'G5',duration:0.06,velocity:0.9},{type:'rest',duration:0.03},{type:'note',note:'C5',duration:0.06,velocity:0.7},{type:'note',note:'E5',duration:0.06,velocity:0.8},{type:'note',note:'G5',duration:0.06,velocity:0.9},{type:'note',note:'C6',duration:0.25,velocity:1.0}] },
            { cat:'game', name:'점프', icon:'🦘', style:'액션', duration:'0.3', desc:'짧은 상승 점프 효과',
              synth:'square', adsr:{attack:2,decay:15,sustain:0.2,release:80}, volume:0.5, reverb:0.1,
              notes:[{type:'note',note:'C4',duration:0.05,velocity:0.7},{type:'note',note:'G4',duration:0.10,velocity:0.9}] },
            { cat:'game', name:'아이템 드롭', icon:'📦', style:'드롭', duration:'0.5', desc:'하강 아이템 획득음',
              synth:'triangle', adsr:{attack:3,decay:25,sustain:0.3,release:120}, volume:0.5, reverb:0.2,
              notes:[{type:'note',note:'G5',duration:0.06,velocity:0.8},{type:'note',note:'E5',duration:0.06,velocity:0.7},{type:'note',note:'C5',duration:0.06,velocity:0.6},{type:'note',note:'G4',duration:0.15,velocity:0.9}] },

            // ━━━ UI 인터랙션 (UI) ━━━
            { cat:'ui', name:'버튼 클릭', icon:'👆', style:'탭', duration:'0.1', desc:'미세한 버튼 터치 피드백',
              synth:'sine', adsr:{attack:1,decay:10,sustain:0.1,release:50}, volume:0.3, reverb:0.0,
              notes:[{type:'note',note:'C5',duration:0.05,velocity:0.6}] },
            { cat:'ui', name:'토글 On', icon:'🟢', style:'활성화', duration:'0.2', desc:'상승 2음 토글 켜기',
              synth:'sine', adsr:{attack:3,decay:20,sustain:0.3,release:100}, volume:0.4, reverb:0.1,
              notes:[{type:'note',note:'E5',duration:0.06,velocity:0.6},{type:'note',note:'A5',duration:0.10,velocity:0.8}] },
            { cat:'ui', name:'토글 Off', icon:'🔴', style:'비활성화', duration:'0.2', desc:'하강 2음 토글 끄기',
              synth:'sine', adsr:{attack:3,decay:20,sustain:0.3,release:100}, volume:0.4, reverb:0.1,
              notes:[{type:'note',note:'A5',duration:0.06,velocity:0.6},{type:'note',note:'E5',duration:0.10,velocity:0.5}] },
            { cat:'ui', name:'스와이프', icon:'👉', style:'제스처', duration:'0.3', desc:'부드러운 스와이프 피드백',
              synth:'sine', adsr:{attack:5,decay:15,sustain:0.2,release:80}, volume:0.3, reverb:0.2,
              notes:[{type:'note',note:'G4',duration:0.04,velocity:0.4},{type:'note',note:'B4',duration:0.04,velocity:0.5},{type:'note',note:'D5',duration:0.04,velocity:0.6},{type:'note',note:'G5',duration:0.08,velocity:0.7}] },
            { cat:'ui', name:'팝업 등장', icon:'💡', style:'어텐션', duration:'0.4', desc:'팝업 알림 등장 효과',
              synth:'triangle', adsr:{attack:5,decay:30,sustain:0.4,release:150}, volume:0.5, reverb:0.2,
              notes:[{type:'note',note:'G4',duration:0.08,velocity:0.6},{type:'note',note:'C5',duration:0.08,velocity:0.7},{type:'note',note:'E5',duration:0.15,velocity:0.9}] },
            { cat:'ui', name:'드래그 시작', icon:'✊', style:'그랩', duration:'0.15', desc:'드래그 시작 피드백',
              synth:'sine', adsr:{attack:2,decay:10,sustain:0.2,release:40}, volume:0.3, reverb:0.0,
              notes:[{type:'note',note:'D5',duration:0.04,velocity:0.5},{type:'note',note:'F5',duration:0.06,velocity:0.6}] },

            // ━━━ 브랜드 징글 (Jingle) ━━━
            { cat:'jingle', name:'팡파레', icon:'🎉', style:'축하', duration:'2.0', desc:'화려한 축하 팡파레',
              synth:'triangle', adsr:{attack:10,decay:100,sustain:0.6,release:600}, volume:0.8, reverb:0.4,
              notes:[{type:'note',note:'G4',duration:0.12,velocity:0.7},{type:'note',note:'C5',duration:0.12,velocity:0.8},{type:'note',note:'E5',duration:0.12,velocity:0.9},{type:'rest',duration:0.08},{type:'note',note:'G5',duration:0.20,velocity:1.0},{type:'rest',duration:0.06},{type:'chord',chord:['C5','E5','G5','C6'],duration:0.80,velocity:1.0}] },
            { cat:'jingle', name:'엔딩 징글', icon:'🔚', style:'마무리', duration:'2.0', desc:'깔끔한 하강 마무리 멜로디',
              synth:'sine', adsr:{attack:15,decay:100,sustain:0.6,release:800}, volume:0.7, reverb:0.4,
              notes:[{type:'note',note:'G5',duration:0.20,velocity:0.8},{type:'note',note:'E5',duration:0.20,velocity:0.7},{type:'note',note:'C5',duration:0.20,velocity:0.8},{type:'rest',duration:0.10},{type:'chord',chord:['C4','E4','G4'],duration:0.80,velocity:0.9}] },
            { cat:'jingle', name:'오프닝 징글', icon:'🎪', style:'시작', duration:'2.0', desc:'밝은 상승 오프닝 멜로디',
              synth:'triangle', adsr:{attack:10,decay:80,sustain:0.6,release:500}, volume:0.8, reverb:0.3,
              notes:[{type:'note',note:'C4',duration:0.15,velocity:0.6},{type:'note',note:'E4',duration:0.15,velocity:0.7},{type:'note',note:'G4',duration:0.15,velocity:0.8},{type:'note',note:'C5',duration:0.15,velocity:0.9},{type:'rest',duration:0.05},{type:'chord',chord:['E5','G5','C6'],duration:0.80,velocity:1.0}] },
            { cat:'jingle', name:'멜로디 루프', icon:'🔁', style:'반복 멜로디', duration:'2.0', desc:'중독성 있는 루프 멜로디',
              synth:'sine', adsr:{attack:10,decay:60,sustain:0.5,release:300}, volume:0.6, reverb:0.3,
              notes:[{type:'note',note:'E5',duration:0.20,velocity:0.7},{type:'note',note:'G5',duration:0.15,velocity:0.8},{type:'note',note:'A5',duration:0.15,velocity:0.9},{type:'note',note:'G5',duration:0.20,velocity:0.7},{type:'note',note:'E5',duration:0.15,velocity:0.8},{type:'note',note:'D5',duration:0.15,velocity:0.7},{type:'note',note:'C5',duration:0.30,velocity:0.6}] },
            { cat:'jingle', name:'록 시그널', icon:'🎸', style:'파워 코드', duration:'1.5', desc:'강렬한 파워 코드 시그널',
              synth:'sawtooth', adsr:{attack:5,decay:60,sustain:0.5,release:400}, volume:0.6, reverb:0.3,
              notes:[{type:'chord',chord:['E3','B3','E4'],duration:0.30,velocity:1.0},{type:'rest',duration:0.05},{type:'chord',chord:['G3','D4','G4'],duration:0.30,velocity:0.9},{type:'rest',duration:0.05},{type:'chord',chord:['A3','E4','A4'],duration:0.50,velocity:1.0}] },
            { cat:'jingle', name:'재즈 시그널', icon:'🎷', style:'스윙', duration:'2.0', desc:'세련된 재즈 코드 진행',
              synth:'sine', adsr:{attack:20,decay:120,sustain:0.7,release:600}, volume:0.7, reverb:0.5,
              notes:[{type:'chord',chord:['C4','E4','G4','B4'],duration:0.40,velocity:0.7},{type:'rest',duration:0.10},{type:'chord',chord:['F4','A4','C5','E5'],duration:0.40,velocity:0.8},{type:'rest',duration:0.10},{type:'chord',chord:['G4','B4','D5','F5'],duration:0.40,velocity:0.9},{type:'chord',chord:['C4','E4','G4','C5'],duration:0.60,velocity:1.0}] },

            // ━━━ 방송/미디어 (Broadcast) ━━━
            { cat:'broadcast', name:'로딩 루프', icon:'🔄', style:'반복 패턴', duration:'1.5', desc:'리듬감 있는 4음 대기 루프',
              synth:'sine', adsr:{attack:10,decay:60,sustain:0.4,release:200}, volume:0.5, reverb:0.2,
              notes:[{type:'note',note:'E4',duration:0.15,velocity:0.6},{type:'note',note:'G4',duration:0.15,velocity:0.7},{type:'note',note:'A4',duration:0.15,velocity:0.8},{type:'note',note:'G4',duration:0.15,velocity:0.6}] },
            { cat:'broadcast', name:'뉴스 인트로', icon:'📺', style:'뉴스 시그널', duration:'2.5', desc:'긴장감 있는 뉴스 오프닝',
              synth:'sine', adsr:{attack:15,decay:100,sustain:0.7,release:800}, volume:0.8, reverb:0.4,
              notes:[{type:'chord',chord:['C4','G4'],duration:0.25,velocity:0.7},{type:'chord',chord:['D4','A4'],duration:0.25,velocity:0.8},{type:'chord',chord:['E4','B4'],duration:0.25,velocity:0.9},{type:'rest',duration:0.10},{type:'chord',chord:['C4','E4','G4','C5'],duration:0.80,velocity:1.0}] },
            { cat:'broadcast', name:'카운트다운', icon:'⏱️', style:'카운트', duration:'3.0', desc:'3-2-1 카운트다운 비프',
              synth:'square', adsr:{attack:3,decay:20,sustain:0.3,release:100}, volume:0.5, reverb:0.1,
              notes:[{type:'note',note:'C5',duration:0.10,velocity:0.6},{type:'rest',duration:0.60},{type:'note',note:'C5',duration:0.10,velocity:0.7},{type:'rest',duration:0.60},{type:'note',note:'C5',duration:0.10,velocity:0.8},{type:'rest',duration:0.30},{type:'note',note:'C6',duration:0.40,velocity:1.0}] },
            { cat:'broadcast', name:'브레이킹 뉴스', icon:'🔴', style:'속보', duration:'1.5', desc:'긴급한 속보 알림음',
              synth:'sawtooth', adsr:{attack:5,decay:50,sustain:0.5,release:300}, volume:0.7, reverb:0.2,
              notes:[{type:'chord',chord:['E4','G#4','B4'],duration:0.15,velocity:1.0},{type:'rest',duration:0.05},{type:'chord',chord:['E4','G#4','B4'],duration:0.15,velocity:1.0},{type:'rest',duration:0.05},{type:'chord',chord:['E4','G#4','B4','E5'],duration:0.50,velocity:1.0}] },
            { cat:'broadcast', name:'팟캐스트 인트로', icon:'🎙️', style:'캐주얼', duration:'2.0', desc:'따뜻하고 캐주얼한 인트로',
              synth:'triangle', adsr:{attack:20,decay:80,sustain:0.6,release:400}, volume:0.6, reverb:0.3,
              notes:[{type:'note',note:'G4',duration:0.20,velocity:0.6},{type:'note',note:'B4',duration:0.15,velocity:0.7},{type:'note',note:'D5',duration:0.15,velocity:0.8},{type:'note',note:'G5',duration:0.30,velocity:0.7},{type:'rest',duration:0.10},{type:'chord',chord:['G4','B4','D5'],duration:0.50,velocity:0.8}] },
            { cat:'broadcast', name:'유튜브 인트로', icon:'▶️', style:'에너제틱', duration:'1.5', desc:'에너지 넘치는 인트로',
              synth:'sawtooth', adsr:{attack:5,decay:40,sustain:0.4,release:200}, volume:0.5, reverb:0.2,
              notes:[{type:'note',note:'E4',duration:0.08,velocity:0.7},{type:'note',note:'G4',duration:0.08,velocity:0.8},{type:'note',note:'B4',duration:0.08,velocity:0.9},{type:'rest',duration:0.03},{type:'chord',chord:['E4','G4','B4','E5'],duration:0.40,velocity:1.0}] },
            { cat:'broadcast', name:'라디오 징글', icon:'📻', style:'클래식 라디오', duration:'2.0', desc:'클래식 라디오 스테이션 징글',
              synth:'sine', adsr:{attack:10,decay:80,sustain:0.6,release:500}, volume:0.7, reverb:0.4,
              notes:[{type:'note',note:'C5',duration:0.15,velocity:0.7},{type:'note',note:'D5',duration:0.10,velocity:0.7},{type:'note',note:'E5',duration:0.15,velocity:0.8},{type:'note',note:'G5',duration:0.10,velocity:0.8},{type:'note',note:'A5',duration:0.15,velocity:0.9},{type:'rest',duration:0.05},{type:'chord',chord:['C5','E5','G5'],duration:0.60,velocity:1.0}] },
        ],

        // ===== Init =====
        init() {
            this.loadSavedSounds();
            // 기본 빈 시퀀스
            if (this.notes.length === 0) {
                this.notes = [
                    { type:'note', note:'C5', duration: 0.20, velocity: 0.8 },
                    { type:'note', note:'E5', duration: 0.20, velocity: 0.9 },
                    { type:'note', note:'G5', duration: 0.15, velocity: 0.7 },
                    { type:'rest', duration: 0.05 },
                    { type:'chord', chord:['C5','E5','G5'], duration: 0.80, velocity: 1.0 },
                ];
            }
        },

        getAudioCtx() {
            if (!this.audioCtx) {
                this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
                // 마스터 컴프레서 (클리핑 방지)
                const comp = this.audioCtx.createDynamicsCompressor();
                comp.threshold.value = -6;
                comp.knee.value = 10;
                comp.ratio.value = 12;
                comp.attack.value = 0.003;
                comp.release.value = 0.15;
                comp.connect(this.audioCtx.destination);
                this._masterNode = comp;
            }
            return this.audioCtx;
        },

        getMasterNode() {
            const ctx = this.getAudioCtx();
            return this._masterNode || ctx.destination;
        },

        // ===== Note helpers =====
        getNoteLabel(n) {
            if (n.type === 'rest') return '쉼표';
            if (n.type === 'chord') return (n.chord || []).join('+');
            return n.note || 'C4';
        },

        getNoteBarHeight(n) {
            if (n.type === 'rest') return 20;
            const note = n.type === 'chord' ? (n.chord?.[0] || 'C4') : (n.note || 'C4');
            const idx = this.allNotes.indexOf(note);
            return Math.max(20, 15 + (idx >= 0 ? idx : 7) * 3.5);
        },

        getTotalDuration() {
            return this.notes.reduce((sum, n) => sum + (n.duration || 0), 0);
        },

        addNote(type) {
            if (type === 'rest') {
                this.notes.push({ type: 'rest', duration: 0.10 });
            } else {
                this.notes.push({ type: 'note', note: 'C5', duration: 0.20, velocity: 0.8 });
            }
            this.pickerIdx = -1;
        },

        removeNote(i) {
            this.notes.splice(i, 1);
            this.pickerIdx = -1;
            this.selectedIdx = -1;
        },

        updateNoteValue(i, nn) {
            if (this.notes[i].type === 'rest') return;
            if (this.notes[i].type === 'chord') {
                if (!this.notes[i].chord) this.notes[i].chord = [];
                this.notes[i].chord.push(nn);
            } else {
                this.notes[i].note = nn;
            }
        },

        // ===== Play =====
        async playNote(n, ctx, startTime) {
            if (n.type === 'rest') return;

            const freqs = n.type === 'chord'
                ? (n.chord || []).map(c => this.noteFreq[c]).filter(Boolean)
                : [this.noteFreq[n.note]].filter(Boolean);

            if (freqs.length === 0) return;

            const vel = (n.velocity || 0.8) * this.volume;
            const a = this.adsr.attack / 1000;
            const d = this.adsr.decay / 1000;
            const s = this.adsr.sustain;
            const r = this.adsr.release / 1000;
            const dur = n.duration || 0.2;

            freqs.forEach(freq => {
                const osc = ctx.createOscillator();
                const gain = ctx.createGain();

                osc.type = this.synth;
                osc.frequency.setValueAtTime(freq, startTime);

                // ADSR
                gain.gain.setValueAtTime(0, startTime);
                gain.gain.linearRampToValueAtTime(vel, startTime + a);
                gain.gain.linearRampToValueAtTime(vel * s, startTime + a + d);
                gain.gain.setValueAtTime(vel * s, startTime + dur);
                gain.gain.exponentialRampToValueAtTime(0.001, startTime + dur + r);

                osc.connect(gain).connect(this.getMasterNode());
                osc.start(startTime);
                osc.stop(startTime + dur + r + 0.05);
            });
        },

        async playAll() {
            if (this.isPlaying) return;
            if (this.notes.length === 0 && !this.voiceBuffer && !this.bgmBuffer) {
                return this.toast('재생할 콘텐츠가 없습니다.\n음표를 추가하거나 음성/배경음악을 먼저 생성해 주세요.', 'warn');
            }
            this.isPlaying = true;
            this.pickerIdx = -1;

            const ctx = this.getAudioCtx();
            if (ctx.state === 'suspended') await ctx.resume();

            let t = ctx.currentTime + 0.05;
            const startTime = t;

            // AI BGM이 있으면 시퀀서(수동/프리셋) 제외, 없으면 시퀀서 재생
            const useBgm = !!this.bgmBuffer;

            if (!useBgm) {
                // 시퀀서 모드: 수동/프리셋 음표 재생
                this.notes.forEach((n, i) => {
                    const noteStart = t;
                    this.playNote(n, ctx, noteStart);
                    const delayMs = (noteStart - startTime) * 1000;
                    setTimeout(() => { this.playingIdx = i; }, delayMs);
                    t += n.duration || 0.2;
                });
            } else {
                // BGM 모드: AI 배경음악 재생
                const bgmSrc = ctx.createBufferSource();
                bgmSrc.buffer = this.bgmBuffer;
                const bgmGain = ctx.createGain();
                bgmGain.gain.value = this.bgmVolume;
                bgmSrc.connect(bgmGain).connect(this.getMasterNode());
                bgmSrc.start(startTime);
            }

            // Voice(TTS)는 공통 — 양쪽 모드 모두 재생
            if (this.voiceBuffer) {
                const voiceSrc = ctx.createBufferSource();
                voiceSrc.buffer = this.voiceBuffer;
                const voiceGain = ctx.createGain();
                voiceGain.gain.value = this.voiceVolume;
                voiceSrc.connect(voiceGain).connect(this.getMasterNode());
                voiceSrc.start(startTime + this.voiceDelay);
            }

            // Draw waveform
            this.drawWaveform(ctx);

            const synthMs = !useBgm ? (t - startTime) * 1000 + (this.adsr.release || 500) : 0;
            const voiceMs = this.voiceBuffer ? (this.voiceDelay + this.voiceBuffer.duration) * 1000 + 200 : 0;
            const bgmMs = useBgm ? this.bgmBuffer.duration * 1000 + 200 : 0;
            const totalMs = Math.max(synthMs, voiceMs, bgmMs);
            setTimeout(() => {
                this.isPlaying = false;
                this.playingIdx = -1;
            }, totalMs);
        },

        stopAll() {
            if (this.audioCtx) {
                this.audioCtx.close();
                this.audioCtx = null;
                this._masterNode = null;
            }
            this.isPlaying = false;
            this.playingIdx = -1;
        },

        // ===== Waveform =====
        drawWaveform(ctx) {
            const canvas = this.$refs.waveformCanvas;
            if (!canvas) return;
            const c = canvas.getContext('2d');
            canvas.width = canvas.offsetWidth * 2;
            canvas.height = canvas.offsetHeight * 2;

            const analyser = ctx.createAnalyser();
            analyser.fftSize = 2048;

            // Connect to destination via analyser
            const bufLen = analyser.frequencyBinCount;
            const dataArr = new Uint8Array(bufLen);

            const draw = () => {
                if (!this.isPlaying) {
                    // Clear
                    c.fillStyle = '#1e293b';
                    c.fillRect(0, 0, canvas.width, canvas.height);
                    return;
                }
                requestAnimationFrame(draw);
                analyser.getByteTimeDomainData(dataArr);

                c.fillStyle = '#1e293b';
                c.fillRect(0, 0, canvas.width, canvas.height);
                c.lineWidth = 2;
                c.strokeStyle = '#6366f1';
                c.beginPath();

                const sliceW = canvas.width / bufLen;
                let x = 0;
                for (let i = 0; i < bufLen; i++) {
                    const v = dataArr[i] / 128.0;
                    const y = v * canvas.height / 2;
                    i === 0 ? c.moveTo(x, y) : c.lineTo(x, y);
                    x += sliceW;
                }
                c.lineTo(canvas.width, canvas.height / 2);
                c.stroke();
            };
            draw();
        },

        // ===== Export WAV =====
        async exportWav() {
            if (this.notes.length === 0 && !this.voiceBuffer && !this.bgmBuffer) {
                return this.toast('저장할 콘텐츠가 없습니다.\n음표, 음성, 배경음악 중 하나 이상을 먼저 만들어 주세요.', 'warn');
            }

            const sampleRate = 44100;
            const useBgm = !!this.bgmBuffer;
            const synthDur = !useBgm ? this.getTotalDuration() + this.adsr.release / 1000 + 0.5 : 0;
            const voiceDur = this.voiceBuffer ? this.voiceDelay + this.voiceBuffer.duration + 0.5 : 0;
            const bgmDur = useBgm ? this.bgmBuffer.duration + 0.5 : 0;
            const totalDur = Math.max(synthDur, voiceDur, bgmDur, 0.5);
            const offline = new OfflineAudioContext(2, sampleRate * totalDur, sampleRate);

            // 오프라인 컴프레서
            const offComp = offline.createDynamicsCompressor();
            offComp.threshold.value = -6;
            offComp.knee.value = 10;
            offComp.ratio.value = 12;
            offComp.attack.value = 0.003;
            offComp.release.value = 0.15;
            offComp.connect(offline.destination);

            let t = 0.05;

            if (!useBgm) {
                // 시퀀서 모드: 수동/프리셋 음표만
                this.notes.forEach(n => {
                    if (n.type !== 'rest') {
                        const freqs = n.type === 'chord'
                            ? (n.chord || []).map(c => this.noteFreq[c]).filter(Boolean)
                            : [this.noteFreq[n.note]].filter(Boolean);

                        const vel = (n.velocity || 0.8) * this.volume;
                        const a = this.adsr.attack / 1000;
                        const d = this.adsr.decay / 1000;
                        const s = this.adsr.sustain;
                        const r = this.adsr.release / 1000;
                        const dur = n.duration || 0.2;

                        freqs.forEach(freq => {
                            const osc = offline.createOscillator();
                            const gain = offline.createGain();
                            osc.type = this.synth;
                            osc.frequency.setValueAtTime(freq, t);
                            gain.gain.setValueAtTime(0, t);
                            gain.gain.linearRampToValueAtTime(vel, t + a);
                            gain.gain.linearRampToValueAtTime(vel * s, t + a + d);
                            gain.gain.setValueAtTime(vel * s, t + dur);
                            gain.gain.exponentialRampToValueAtTime(0.001, t + dur + r);
                            osc.connect(gain).connect(offComp);
                            osc.start(t);
                            osc.stop(t + dur + r + 0.05);
                        });
                    }
                    t += n.duration || 0.2;
                });
            } else {
                // BGM 모드: AI 배경음악만
                const bgmSrc = offline.createBufferSource();
                bgmSrc.buffer = this.bgmBuffer;
                const bgmGain = offline.createGain();
                bgmGain.gain.value = this.bgmVolume;
                bgmSrc.connect(bgmGain).connect(offComp);
                bgmSrc.start(0.05);
            }

            // Voice(TTS)는 공통
            if (this.voiceBuffer) {
                const voiceSrc = offline.createBufferSource();
                voiceSrc.buffer = this.voiceBuffer;
                const voiceGain = offline.createGain();
                voiceGain.gain.value = this.voiceVolume;
                voiceSrc.connect(voiceGain).connect(offComp);
                voiceSrc.start(0.05 + this.voiceDelay);
            }

            const buffer = await offline.startRendering();
            const wav = this.bufferToWav(buffer);
            const blob = new Blob([wav], { type: 'audio/wav' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'sound-logo-' + new Date().toISOString().slice(0,10) + '.wav';
            a.click();
            URL.revokeObjectURL(url);
            this.toast('WAV 파일이 다운로드됩니다');
        },

        bufferToWav(buffer) {
            const numCh = buffer.numberOfChannels;
            const len = buffer.length;
            const sampleRate = buffer.sampleRate;
            const bitsPerSample = 16;
            const byteRate = sampleRate * numCh * bitsPerSample / 8;
            const blockAlign = numCh * bitsPerSample / 8;
            const dataSize = len * blockAlign;

            const buf = new ArrayBuffer(44 + dataSize);
            const view = new DataView(buf);

            const writeStr = (offset, str) => { for(let i=0;i<str.length;i++) view.setUint8(offset+i, str.charCodeAt(i)); };
            writeStr(0, 'RIFF');
            view.setUint32(4, 36 + dataSize, true);
            writeStr(8, 'WAVE');
            writeStr(12, 'fmt ');
            view.setUint32(16, 16, true);
            view.setUint16(20, 1, true);
            view.setUint16(22, numCh, true);
            view.setUint32(24, sampleRate, true);
            view.setUint32(28, byteRate, true);
            view.setUint16(32, blockAlign, true);
            view.setUint16(34, bitsPerSample, true);
            writeStr(36, 'data');
            view.setUint32(40, dataSize, true);

            let offset = 44;
            for (let i = 0; i < len; i++) {
                for (let ch = 0; ch < numCh; ch++) {
                    let sample = buffer.getChannelData(ch)[i];
                    sample = Math.max(-1, Math.min(1, sample));
                    view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7FFF, true);
                    offset += 2;
                }
            }
            return buf;
        },

        // ===== 음성 오버레이 (TTS) =====
        async generateVoice() {
            if (this.voiceLoading) return;
            if (!this.voiceText.trim()) return this.toast('음성으로 변환할 텍스트를 입력해 주세요.', 'warn');
            this.voiceLoading = true;
            this.voiceBuffer = null;

            try {
                const res = await fetch('{{ route("rd.sound-logo.tts") }}', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
                    },
                    body: JSON.stringify({
                        text: this.voiceText,
                        voice_name: this.voiceName,
                        voice_category: this.voiceCategory,
                        voice_style: this.voiceStyle,
                        voice_speed: this.voiceSpeed,
                    }),
                });

                const data = await res.json();
                if (data.success && data.audio_data) {
                    this.voiceAudioData = data.audio_data;
                    this.voiceMimeType = data.mime_type || 'audio/L16;rate=24000';
                    // 샘플레이트 파싱
                    const rateMatch = this.voiceMimeType.match(/rate=(\d+)/);
                    const sampleRate = rateMatch ? parseInt(rateMatch[1]) : 24000;
                    this.voiceBuffer = this.decodeL16(data.audio_data, sampleRate);
                    this.toast('음성 생성 완료: "' + this.voiceText + '"');
                } else {
                    this.toast(data.error || '음성 생성에 실패했습니다. 다시 시도해 주세요.', 'error');
                }
            } catch (e) {
                this.toast('음성 생성 중 네트워크 오류가 발생했습니다.', 'error');
            } finally {
                this.voiceLoading = false;
            }
        },

        decodeL16(base64Data, sampleRate) {
            const binaryStr = atob(base64Data);
            const bytes = new Uint8Array(binaryStr.length);
            for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);

            const view = new DataView(bytes.buffer);
            const numSamples = Math.floor(bytes.length / 2);
            const ctx = this.getAudioCtx();
            const buffer = ctx.createBuffer(1, numSamples, sampleRate);
            const ch = buffer.getChannelData(0);

            for (let i = 0; i < numSamples; i++) {
                ch[i] = view.getInt16(i * 2, true) / 32768; // Gemini TTS = s16le (little-endian)
            }
            return buffer;
        },

        async playVoiceOnly() {
            if (!this.voiceBuffer) return this.toast('음성을 먼저 생성해 주세요.\n음성 탭에서 텍스트를 입력하고 생성 버튼을 눌러주세요.', 'warn');
            const ctx = this.getAudioCtx();
            if (ctx.state === 'suspended') await ctx.resume();
            const src = ctx.createBufferSource();
            src.buffer = this.voiceBuffer;
            const gain = ctx.createGain();
            gain.gain.value = this.voiceVolume;
            src.connect(gain).connect(this.getMasterNode());
            src.start();
        },

        clearVoice() {
            this.voiceBuffer = null;
            this.voiceAudioData = null;
            this.voiceMimeType = '';
            this.toast('음성 삭제됨');
        },

        // ===== 배경음악 (Lyria RealTime) =====
        async generateBgm() {
            if (this.bgmLoading) return;
            if (!this.bgmPrompt.trim()) return this.toast('배경음악 프롬프트를 입력해 주세요.\n원하는 분위기나 장르를 설명해 주세요.', 'warn');
            this.bgmLoading = true;
            this.bgmError = '';
            this.bgmProgress = '연결 중...';

            try {
                // 1) 서버에서 API 설정 가져오기
                const cfgRes = await fetch('{{ route("rd.sound-logo.lyria-config") }}');
                const cfg = await cfgRes.json();
                if (!cfg.success) throw new Error(cfg.error || 'API 설정 로드 실패');

                // 2) WebSocket 연결
                const wsUrl = cfg.ws_url + '?key=' + cfg.api_key;
                console.log('[Lyria] WebSocket 연결 시작:', cfg.ws_url);
                console.log('[Lyria] Model:', cfg.model, '| Prompt:', this.bgmPrompt);
                const ws = new WebSocket(wsUrl);
                this.bgmWs = ws;

                const audioChunks = [];
                const duration = this.bgmDuration;
                let setupDone = false;
                let playStartTime = null;
                let messageCount = 0;

                ws.onopen = () => {
                    console.log('[Lyria] WebSocket 연결됨, setup 전송');
                    this.bgmProgress = '초기화 중...';
                    ws.send(JSON.stringify({
                        setup: { model: cfg.model }
                    }));
                };

                ws.onmessage = async (event) => {
                    messageCount++;
                    let raw = event.data;
                    const isBlob = raw instanceof Blob;
                    if (isBlob) {
                        console.log('[Lyria] 메시지 #' + messageCount + ': Blob (size=' + raw.size + ')');
                        raw = await raw.text();
                    }
                    let msg;
                    try {
                        msg = JSON.parse(raw);
                    } catch (e) {
                        console.warn('[Lyria] 메시지 #' + messageCount + ': JSON 파싱 실패', typeof raw, raw.substring?.(0, 200));
                        return;
                    }

                    console.log('[Lyria] 메시지 #' + messageCount + ':', Object.keys(msg).join(', '));

                    // Setup 완료
                    if (msg.setupComplete) {
                        setupDone = true;
                        console.log('[Lyria] Setup 완료, 프롬프트/설정/재생 전송');
                        this.bgmProgress = '프롬프트 설정 중...';

                        ws.send(JSON.stringify({
                            clientContent: {
                                weightedPrompts: [{ text: this.bgmPrompt, weight: 1.0 }]
                            }
                        }));

                        ws.send(JSON.stringify({
                            musicGenerationConfig: {
                                bpm: this.bgmBpm,
                                density: this.bgmDensity / 100,
                                brightness: this.bgmBrightness / 100,
                                scale: this.bgmScale,
                                temperature: 1.0,
                            }
                        }));

                        ws.send(JSON.stringify({
                            playbackControl: 'PLAY'
                        }));
                        playStartTime = Date.now();
                        this.bgmProgress = '음악 생성 중... 0/' + duration + '초';
                    }

                    // 오디오 청크 수신
                    if (msg.serverContent?.audioChunks) {
                        for (const chunk of msg.serverContent.audioChunks) {
                            if (chunk.data) {
                                audioChunks.push(chunk.data);
                            }
                        }
                        if (audioChunks.length % 10 === 1) {
                            console.log('[Lyria] 오디오 청크 수신:', audioChunks.length, '개');
                        }
                        if (playStartTime) {
                            const elapsed = ((Date.now() - playStartTime) / 1000).toFixed(0);
                            this.bgmProgress = '음악 생성 중... ' + elapsed + '/' + duration + '초';
                        }
                    }

                    // 에러 응답
                    if (msg.error) {
                        console.error('[Lyria] API 에러:', JSON.stringify(msg.error));
                        this.bgmError = msg.error.message || 'Lyria API 에러';
                        this.bgmLoading = false;
                        this.toast(this.bgmError, 'error');
                        ws.close();
                    }

                    // 프롬프트 필터링 경고
                    if (msg.filteredPrompt) {
                        console.warn('[Lyria] 프롬프트 필터링됨');
                        this.bgmError = '프롬프트가 안전 필터에 의해 거부되었습니다.';
                        this.bgmLoading = false;
                        this.toast('입력한 프롬프트가 안전 필터에 거부되었습니다.\n다른 표현으로 변경해 주세요.', 'error');
                        ws.close();
                    }
                };

                ws.onerror = (e) => {
                    console.error('[Lyria] WebSocket 에러:', e);
                    this.bgmError = 'WebSocket 연결 오류가 발생했습니다.';
                    this.bgmLoading = false;
                    this.toast('배경음악 서버 연결에 실패했습니다.\n잠시 후 다시 시도해 주세요.', 'error');
                };

                ws.onclose = (e) => {
                    console.log('[Lyria] WebSocket 종료: code=' + e.code + ', reason=' + e.reason + ', wasClean=' + e.wasClean);
                    console.log('[Lyria] 수신 메시지: ' + messageCount + '개, 오디오 청크: ' + audioChunks.length + '개, setupDone=' + setupDone);
                    this.bgmWs = null;
                    if (audioChunks.length > 0 && !this.bgmError) {
                        this.bgmProgress = '오디오 처리 중...';
                        this.decodeBgmChunks(audioChunks);
                    } else if (!this.bgmError) {
                        this.bgmError = '오디오 데이터를 받지 못했습니다.';
                        this.bgmLoading = false;
                        this.toast('배경음악 생성에 실패했습니다.\n다른 프롬프트로 다시 시도해 주세요.', 'error');
                    }
                };

                // 지정 시간 후 정지
                setTimeout(() => {
                    if (ws.readyState === WebSocket.OPEN) {
                        ws.send(JSON.stringify({
                            playbackControl: 'STOP'
                        }));
                        setTimeout(() => {
                            if (ws.readyState === WebSocket.OPEN) ws.close();
                        }, 500);
                    }
                }, duration * 1000 + 2000); // 약간의 여유 시간

            } catch (e) {
                this.bgmError = e.message || '배경음악 생성 중 오류 발생';
                this.bgmLoading = false;
                this.toast(this.bgmError, 'error');
            }
        },

        async decodeBgmChunks(chunks) {
            try {
                console.log('[Lyria] 오디오 디코딩 시작: ' + chunks.length + '개 청크');
                // base64 청크들을 합쳐서 하나의 바이트 배열로 변환
                const allBytes = [];
                for (const chunk of chunks) {
                    const binaryStr = atob(chunk);
                    for (let i = 0; i < binaryStr.length; i++) {
                        allBytes.push(binaryStr.charCodeAt(i));
                    }
                }
                const pcmData = new Uint8Array(allBytes);
                console.log('[Lyria] 전체 PCM 크기:', pcmData.length, 'bytes');

                if (pcmData.length < 4) {
                    this.bgmError = '유효한 오디오 데이터가 없습니다.';
                    this.bgmLoading = false;
                    return;
                }

                const ctx = this.getAudioCtx();

                // WAV 헤더 감지 (RIFF 매직넘버) → decodeAudioData 사용
                const hasWavHeader = pcmData[0] === 0x52 && pcmData[1] === 0x49 && pcmData[2] === 0x46 && pcmData[3] === 0x46;
                if (hasWavHeader) {
                    console.log('[Lyria] WAV 헤더 감지, decodeAudioData 사용');
                    const buffer = await ctx.decodeAudioData(pcmData.buffer.slice(0));
                    this.bgmBuffer = buffer;
                    this.toast('배경음악 생성 완료 (' + buffer.duration.toFixed(1) + '초)');
                    return;
                }

                // Raw PCM: Lyria RealTime — 16-bit PCM, stereo, 48kHz (little-endian)
                console.log('[Lyria] Raw PCM 디코딩: 48kHz, stereo, 16-bit LE');
                const sampleRate = 48000;
                const numChannels = 2;
                const bytesPerSample = 2;
                const numSamples = Math.floor(pcmData.length / (numChannels * bytesPerSample));

                if (numSamples === 0) {
                    this.bgmError = '유효한 오디오 데이터가 없습니다.';
                    this.bgmLoading = false;
                    return;
                }

                const buffer = ctx.createBuffer(numChannels, numSamples, sampleRate);
                const view = new DataView(pcmData.buffer);

                for (let ch = 0; ch < numChannels; ch++) {
                    const channelData = buffer.getChannelData(ch);
                    for (let i = 0; i < numSamples; i++) {
                        const byteOffset = (i * numChannels + ch) * bytesPerSample;
                        if (byteOffset + 1 < pcmData.length) {
                            channelData[i] = view.getInt16(byteOffset, true) / 32768;
                        }
                    }
                }

                this.bgmBuffer = buffer;
                this.toast('배경음악 생성 완료 (' + buffer.duration.toFixed(1) + '초)');
            } catch (e) {
                console.error('[Lyria] 오디오 디코딩 실패:', e);
                this.bgmError = '오디오 디코딩 실패: ' + e.message;
                this.toast('배경음악 오디오 처리에 실패했습니다.', 'error');
            } finally {
                this.bgmLoading = false;
            }
        },

        async playBgmOnly() {
            if (!this.bgmBuffer) return this.toast('배경음악을 먼저 생성해 주세요.\nAI 배경음악 탭에서 프롬프트를 입력하고 생성 버튼을 눌러주세요.', 'warn');
            const ctx = this.getAudioCtx();
            if (ctx.state === 'suspended') await ctx.resume();
            const src = ctx.createBufferSource();
            src.buffer = this.bgmBuffer;
            const gain = ctx.createGain();
            gain.gain.value = this.bgmVolume;
            src.connect(gain).connect(this.getMasterNode());
            src.start();
        },

        async exportBgmWav() {
            if (!this.bgmBuffer) return this.toast('배경음악을 먼저 생성해 주세요.', 'warn');
            const wav = this.bufferToWav(this.bgmBuffer);
            const blob = new Blob([wav], { type: 'audio/wav' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'bgm-lyria-' + new Date().toISOString().slice(0,10) + '.wav';
            a.click();
            URL.revokeObjectURL(url);
            this.toast('배경음악 WAV 다운로드');
        },

        clearBgm() {
            if (this.bgmWs && this.bgmWs.readyState === WebSocket.OPEN) {
                this.bgmWs.close();
            }
            this.bgmBuffer = null;
            this.bgmLoading = false;
            this.bgmError = '';
            this.toast('배경음악 삭제됨');
        },

        // ===== AI 어시스트 =====
        async generateWithAi() {
            if (this.aiLoading) return;
            if (!this.aiPrompt.trim()) return this.toast('AI 생성 프롬프트를 입력해 주세요.\n원하는 사운드 로고를 설명해 주세요.', 'warn');
            this.aiLoading = true;
            this.aiResult = null;
            this.aiError = '';

            try {
                const res = await fetch('{{ route("rd.sound-logo.generate") }}', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
                    },
                    body: JSON.stringify({
                        prompt: this.aiPrompt,
                        category: this.aiCategory,
                        duration: this.aiDurationRaw / 10,
                    }),
                });

                const data = await res.json();
                if (data.success && data.data) {
                    this.aiResult = data.data;
                    this.toast('AI 사운드 로고가 생성되었습니다.');
                } else {
                    this.aiError = data.error || 'AI 생성에 실패했습니다.';
                    this.toast(this.aiError, 'error');
                }
            } catch (e) {
                this.aiError = '네트워크 오류가 발생했습니다.';
                this.toast(this.aiError, 'error');
            } finally {
                this.aiLoading = false;
            }
        },

        loadAiResult() {
            if (!this.aiResult) return this.toast('적용할 AI 결과가 없습니다.\n먼저 AI 생성을 실행해 주세요.', 'warn');
            const r = this.aiResult;
            if (r.synth) this.synth = r.synth;
            if (r.adsr) this.adsr = { ...r.adsr };
            if (r.volume != null) this.volume = r.volume;
            if (r.reverb != null) this.reverb = r.reverb;
            if (r.notes) this.notes = JSON.parse(JSON.stringify(r.notes));
            this.mode = 'manual';
            this.toast((r.name || 'AI 사운드') + ' 로드됨');
        },

        async previewAiResult() {
            if (this.isPlaying) return;
            if (!this.aiResult) return this.toast('AI 생성 결과가 없습니다.\nAI 생성 탭에서 먼저 사운드를 생성해 주세요.', 'warn');
            // 임시로 AI 결과를 현재 설정에 적용 후 재생
            const backup = { synth: this.synth, adsr: { ...this.adsr }, volume: this.volume, reverb: this.reverb, notes: JSON.parse(JSON.stringify(this.notes)) };
            this.loadAiResult();
            await this.playAll();
            // 재생 완료 후 복원하지 않음 (로드된 상태 유지)
        },

        // ===== Presets =====
        loadPreset(i) {
            const p = this.presets[i];
            this.presetIdx = i;
            this.synth = p.synth;
            this.adsr = { ...p.adsr };
            this.volume = p.volume;
            this.reverb = p.reverb;
            this.notes = JSON.parse(JSON.stringify(p.notes));
            this.mode = 'manual';
            this.toast(p.name + ' 로드됨');
        },

        // ===== Save/Load =====
        loadSavedSounds() {
            try {
                const d = localStorage.getItem('sl_sounds');
                this.savedSounds = d ? JSON.parse(d) : [];
            } catch { this.savedSounds = []; }
        },

        persistSounds() {
            localStorage.setItem('sl_sounds', JSON.stringify(this.savedSounds));
        },

        saveCurrentSound() {
            const name = prompt('사운드 이름을 입력하세요:', '새 사운드');
            if (!name) return;
            const snd = {
                id: 'snd_' + Date.now(),
                name,
                synth: this.synth,
                adsr: { ...this.adsr },
                volume: this.volume,
                reverb: this.reverb,
                notes: JSON.parse(JSON.stringify(this.notes)),
                createdAt: new Date().toISOString(),
            };
            this.savedSounds.push(snd);
            this.currentSoundId = snd.id;
            this.persistSounds();
            this.toast('저장됨: ' + name);
        },

        loadSound(snd) {
            this.currentSoundId = snd.id;
            this.synth = snd.synth;
            this.adsr = { ...snd.adsr };
            this.volume = snd.volume || 0.8;
            this.reverb = snd.reverb || 0.2;
            this.notes = JSON.parse(JSON.stringify(snd.notes));
            this.mode = 'manual';
            this.toast(snd.name + ' 로드됨');
        },

        deleteSound(idx) {
            if (!confirm('삭제하시겠습니까?')) return;
            this.savedSounds.splice(idx, 1);
            this.persistSounds();
            this.toast('삭제됨');
        },

        // ===== JSON Import/Export =====
        exportJson() {
            const data = { synth: this.synth, adsr: this.adsr, volume: this.volume, reverb: this.reverb, notes: this.notes };
            const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'sound-logo-' + new Date().toISOString().slice(0,10) + '.json';
            a.click();
            URL.revokeObjectURL(url);
            this.toast('JSON 내보내기 완료');
        },

        importJson() {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.json';
            input.onchange = (e) => {
                const file = e.target.files[0];
                if (!file) return;
                const reader = new FileReader();
                reader.onload = (ev) => {
                    try {
                        const data = JSON.parse(ev.target.result);
                        if (data.notes) this.notes = data.notes;
                        if (data.synth) this.synth = data.synth;
                        if (data.adsr) this.adsr = { ...data.adsr };
                        if (data.volume != null) this.volume = data.volume;
                        if (data.reverb != null) this.reverb = data.reverb;
                        this.mode = 'manual';
                        this.toast('불러오기 완료');
                    } catch { this.toast('JSON 파일 오류'); }
                };
                reader.readAsText(file);
            };
            input.click();
        },

        toast(msg, type = 'info') {
            this.toastMsg = msg;
            this.toastType = type;
            const dur = type === 'error' ? 3500 : type === 'warn' ? 3000 : 2500;
            setTimeout(() => { this.toastMsg = ''; }, dur);
        },
    };
}
</script>
@endpush
