앱을 켜고 처음 알람이 울릴 때는 소리가 뭉개지거나, 앞부분이 잘려서 나오거나, 아예 안 들리는 경우가 있습니다. 그런데 같은 코드로 두 번째, 세 번째 알람을 재생하면 멀쩡하게 잘 들립니다.
브라우저(그리고 OS)의 오디오 출력 경로는 계속 켜져 있지 않습니다. 실제 스피커나 오디오 드라이버 쪽에서는 소리가 한동안 나오지 않으면 절전 상태로 들어가고, 다시 소리를 출력하라는 신호가 오면 하드웨어를 깨우는 과정을 거칩니다.
문제는 이 깨어나는 과정에 시간이 걸린다는 점입니다. AudioContext를 새로 만들거나, 한동안 소리를 내지 않다가 갑자기 osc.start()를 호출하면, 오디오 파이프라인이 완전히 준비되기 전 짧은 구간 동안 출력이 손실되거나 왜곡됩니다. 알람처럼 "짧고 정확한 타이밍의 소리"를 재생하는 경우, 하필 그 손실 구간이 소리의 시작 부분과 겹쳐서 정작 중요한 첫 비트가 날아가는 현상이 발생하는 것입니다.
해결 아이디어: 진짜 소리를 내기 전에 "예열"한다
function warmSound() {
try {
const ctx = ensureAudioCtx();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.value = 5; // 5Hz, 사실상 들리지 않는 저역
gain.gain.value = 0.0001; // 최소 볼륨 (사람이 거의 못 듣는 수준)
osc.connect(gain).connect(ctx.destination);
osc.start(ctx.currentTime); // 지연 없이 즉시 출력
osc.stop(ctx.currentTime + 0.5); // 0.5초간 유지 후 정지
} catch (e) {
/* 워밍업 실패는 무시 - 워밍업은 보조 수단일 뿐, 실패해도 본 알람 재생에 영향 없어야 함 */
}
}
실제 알람 재생 흐름
async function playAlarmBeep() {
if (!alarmEnabled) return;
const soundChoice = localStorage.getItem(ALARM_SOUND_KEY) || 'beep';
await ensureRunning(); // AudioContext resume 등 상태 확인
warmSound(); // 본 소리 재생 전, 오디오 하드웨어 예열
if (soundChoice === 'dingdong') {
setTimeout(playDingdong, 500);
} else {
setTimeout(playBeepTone, 500);
}
}
function playBeepTone() {
try {
const ctx = ensureAudioCtx();
const WARMUP_DELAY = 0; // 이미 warmSound에서 예열했으므로 추가 지연 없음
const BEEP_DURATION = 1; // 1초간 재생
const startAt = ctx.currentTime + WARMUP_DELAY;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.value = 880; // A5음
// 갑자기 소리가 튀지 않도록 짧게 페이드 인 / 페이드 아웃
gain.gain.setValueAtTime(0.0001, startAt);
gain.gain.exponentialRampToValueAtTime(0.2, startAt + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, startAt + BEEP_DURATION - 0.05);
osc.connect(gain).connect(ctx.destination);
osc.start(startAt);
osc.stop(startAt + BEEP_DURATION);
} catch (e) {
/* 오디오 재생 실패는 무시 (자동재생 정책 등) */
}
}
여기서 gain.gain.exponentialRampToValueAtTime을 이용해 볼륨을 서서히 올리고 내리는 것도 눈여겨볼 부분입니다. 워밍업으로 하드웨어 자체는 깨워뒀더라도, 볼륨이 순간적으로 0에서 최대치로 튀면 노이즈가 발생할 수 있기 때문에, 페이드 인/아웃을 함께 적용해 자연스러운 소리를 만들어줍니다.
흐름을 정리하면 다음과 같습니다.
1. ensureRunning()으로 AudioContext가 정상 동작 중인지 확인 (suspended 상태면 resume)
2. warmSound()를 호출해 0.5초짜리 무음에 가까운 신호를 출력, 오디오 하드웨어를 미리 깨움
3. 워밍업 신호가 끝나는 시점(setTimeout(..., 500))에 맞춰 실제 알람음(playBeepTone 또는 playDingdong)을 재생
즉, "진짜 소리"가 나가기 0.5초 전에 미리 저강도 신호로 파이프라인을 활성화시켜 놓고, 하드웨어가 완전히 깨어난 타이밍에 실제 알람음을 흘려보내는 구조입니다.
시행착오
osc.frequency.value = 0; // 주파수 0
gain.gain.value = 0; // 볼륨 0
주파수가 0이거나 gain이 완전히 0이면, 오디오 그래프상으로는 신호가 흐르지만 실제 하드웨어 출력 레벨에서는 "무음"과 다를 바 없이 처리되어 절전모드를 깨우지 못했습니다. 오디오 드라이버 입장에서는 "출력할 데이터가 없다"고 판단해버리는 것이죠. 즉, 워밍업이 성립하려면 하드웨어가 실제로 "출력 신호가 존재한다"고 인식할 수 있는 최소한의 값이 필요했습니다.
댓글 없음:
댓글 쓰기