fix: warm up AudioContext on first click, clean up code

This commit is contained in:
Mikhail Kiselev
2026-02-10 17:54:31 +03:00
parent 6dac14e0c1
commit 5131a72983
2 changed files with 62 additions and 46 deletions

View File

@@ -35,6 +35,27 @@
EventsOn('timer:meeting_warning', handleMeetingWarning)
EventsOn('timer:meeting_ended', handleMeetingEnded)
EventsOn('timer:speaker_changed', handleSpeakerChanged)
// Warm up AudioContext on first user interaction
const warmUpAudio = async () => {
const ctx = getAudioContext()
if (ctx.state === 'suspended') {
await ctx.resume()
}
// Play silent sound to fully unlock audio
const oscillator = ctx.createOscillator()
const gainNode = ctx.createGain()
oscillator.connect(gainNode)
gainNode.connect(ctx.destination)
gainNode.gain.value = 0 // Silent
oscillator.start()
oscillator.stop(ctx.currentTime + 0.001)
// Remove listener after first interaction
document.removeEventListener('click', warmUpAudio)
document.removeEventListener('keydown', warmUpAudio)
}
document.addEventListener('click', warmUpAudio)
document.addEventListener('keydown', warmUpAudio)
})
async function loadSettings() {
@@ -119,14 +140,9 @@
return audioContext
}
async function playBeep(frequency, duration, type = 'sine') {
function playBeep(frequency, duration, type = 'sine') {
try {
const ctx = getAudioContext()
if (ctx.state === 'suspended') {
await ctx.resume()
// Wait a frame for currentTime to update after resume
await new Promise(resolve => setTimeout(resolve, 50))
}
const oscillator = ctx.createOscillator()
const gainNode = ctx.createGain()
@@ -136,47 +152,36 @@
oscillator.frequency.value = frequency
oscillator.type = type
// Use small offset to ensure sound is scheduled in the future
const startTime = ctx.currentTime + 0.01
gainNode.gain.setValueAtTime(0.3, startTime)
gainNode.gain.exponentialRampToValueAtTime(0.01, startTime + duration)
gainNode.gain.setValueAtTime(0.3, ctx.currentTime)
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + duration)
oscillator.start(startTime)
oscillator.stop(startTime + duration)
oscillator.start(ctx.currentTime)
oscillator.stop(ctx.currentTime + duration)
} catch (e) {
console.error('Failed to play sound:', e)
}
}
async function playSound(name) {
function playSound(name) {
switch (name) {
case 'warning':
// Two short warning beeps
await playBeep(880, 0.15)
playBeep(880, 0.15)
setTimeout(() => playBeep(880, 0.15), 200)
break
case 'timeup':
// Descending tone sequence
await playBeep(1200, 0.2)
playBeep(1200, 0.2)
setTimeout(() => playBeep(900, 0.2), 250)
setTimeout(() => playBeep(600, 0.3), 500)
break
case 'meeting_end':
// Final chime - three notes
await playBeep(523, 0.2) // C5
setTimeout(() => playBeep(659, 0.2), 200) // E5
setTimeout(() => playBeep(784, 0.4), 400) // G5
playBeep(523, 0.2)
setTimeout(() => playBeep(659, 0.2), 200)
setTimeout(() => playBeep(784, 0.4), 400)
break
}
}
async function handleMeetingStarted() {
// Pre-initialize AudioContext on user gesture (meeting start click)
// This is required because AudioContext can only be resumed during user interaction
const ctx = getAudioContext()
if (ctx.state === 'suspended') {
await ctx.resume()
}
function handleMeetingStarted() {
meetingActive = true
currentView = 'main'
}