fix: [sound-logo] 시퀀서/AI BGM 상호 배타적 재생 구조 적용
- AI BGM 있으면 시퀀서(수동/프리셋) 음표 제외 - 시퀀서 모드에서는 AI BGM 제외 - TTS 음성은 양쪽 모두 공통 재생 - exportWav도 동일 로직 적용 + 오프라인 컴프레서 추가
This commit is contained in:
@@ -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' });
|
||||
|
||||
Reference in New Issue
Block a user