diff --git a/resources/views/rd/sound-logo/index.blade.php b/resources/views/rd/sound-logo/index.blade.php index 6eac4f14..2b0f4cf3 100644 --- a/resources/views/rd/sound-logo/index.blade.php +++ b/resources/views/rd/sound-logo/index.blade.php @@ -1488,9 +1488,6 @@ function soundLogo() { if (this.notes.length === 0 && !this.voiceBuffer && !this.bgmBuffer) { return this.toast('재생할 콘텐츠가 없습니다.\n음표를 추가하거나 음성/배경음악을 먼저 생성해 주세요.', 'warn'); } - if (this.notes.length === 0 && !this.voiceBuffer && this.bgmBuffer) { - // BGM만 있는 경우 — 허용하되 안내 - } this.isPlaying = true; this.pickerIdx = -1; @@ -1500,18 +1497,29 @@ function soundLogo() { let t = ctx.currentTime + 0.05; const startTime = t; - this.notes.forEach((n, i) => { - const noteStart = t; - this.playNote(n, ctx, noteStart); + // AI BGM이 있으면 시퀀서(수동/프리셋) 제외, 없으면 시퀀서 재생 + const useBgm = !!this.bgmBuffer; - // Highlight - const delayMs = (noteStart - startTime) * 1000; - setTimeout(() => { this.playingIdx = i; }, delayMs); + 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); + } - t += n.duration || 0.2; - }); - - // Voice overlay + // Voice(TTS)는 공통 — 양쪽 모드 모두 재생 if (this.voiceBuffer) { const voiceSrc = ctx.createBufferSource(); voiceSrc.buffer = this.voiceBuffer; @@ -1521,22 +1529,12 @@ function soundLogo() { voiceSrc.start(startTime + this.voiceDelay); } - // BGM overlay - if (this.bgmBuffer) { - 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); - } - // Draw waveform this.drawWaveform(ctx); - const synthMs = (t - startTime) * 1000 + (this.adsr.release || 500); + 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 = this.bgmBuffer ? this.bgmBuffer.duration * 1000 + 200 : 0; + const bgmMs = useBgm ? this.bgmBuffer.duration * 1000 + 200 : 0; const totalMs = Math.max(synthMs, voiceMs, bgmMs); setTimeout(() => { this.isPlaying = false; @@ -1606,64 +1604,76 @@ function soundLogo() { } const sampleRate = 44100; - const synthDur = this.getTotalDuration() + this.adsr.release / 1000 + 0.5; + 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 = this.bgmBuffer ? this.bgmBuffer.duration + 0.5 : 0; - const totalDur = Math.max(synthDur, voiceDur, bgmDur); + 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; - 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; + 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); - 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(offline.destination); - osc.start(t); - osc.stop(t + dur + r + 0.05); - }); - } - t += n.duration || 0.2; - }); + 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; - // Voice overlay in offline context + 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(offline.destination); + voiceSrc.connect(voiceGain).connect(offComp); voiceSrc.start(0.05 + this.voiceDelay); } - // BGM overlay in offline context - if (this.bgmBuffer) { - const bgmSrc = offline.createBufferSource(); - bgmSrc.buffer = this.bgmBuffer; - const bgmGain = offline.createGain(); - bgmGain.gain.value = this.bgmVolume; - bgmSrc.connect(bgmGain).connect(offline.destination); - bgmSrc.start(0.05); - } - const buffer = await offline.startRendering(); const wav = this.bufferToWav(buffer); const blob = new Blob([wav], { type: 'audio/wav' });