fix: warm up AudioContext on first click, clean up code
This commit is contained in:
@@ -35,6 +35,27 @@
|
|||||||
EventsOn('timer:meeting_warning', handleMeetingWarning)
|
EventsOn('timer:meeting_warning', handleMeetingWarning)
|
||||||
EventsOn('timer:meeting_ended', handleMeetingEnded)
|
EventsOn('timer:meeting_ended', handleMeetingEnded)
|
||||||
EventsOn('timer:speaker_changed', handleSpeakerChanged)
|
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() {
|
async function loadSettings() {
|
||||||
@@ -119,14 +140,9 @@
|
|||||||
return audioContext
|
return audioContext
|
||||||
}
|
}
|
||||||
|
|
||||||
async function playBeep(frequency, duration, type = 'sine') {
|
function playBeep(frequency, duration, type = 'sine') {
|
||||||
try {
|
try {
|
||||||
const ctx = getAudioContext()
|
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 oscillator = ctx.createOscillator()
|
||||||
const gainNode = ctx.createGain()
|
const gainNode = ctx.createGain()
|
||||||
|
|
||||||
@@ -136,47 +152,36 @@
|
|||||||
oscillator.frequency.value = frequency
|
oscillator.frequency.value = frequency
|
||||||
oscillator.type = type
|
oscillator.type = type
|
||||||
|
|
||||||
// Use small offset to ensure sound is scheduled in the future
|
gainNode.gain.setValueAtTime(0.3, ctx.currentTime)
|
||||||
const startTime = ctx.currentTime + 0.01
|
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + duration)
|
||||||
gainNode.gain.setValueAtTime(0.3, startTime)
|
|
||||||
gainNode.gain.exponentialRampToValueAtTime(0.01, startTime + duration)
|
|
||||||
|
|
||||||
oscillator.start(startTime)
|
oscillator.start(ctx.currentTime)
|
||||||
oscillator.stop(startTime + duration)
|
oscillator.stop(ctx.currentTime + duration)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to play sound:', e)
|
console.error('Failed to play sound:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function playSound(name) {
|
function playSound(name) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'warning':
|
case 'warning':
|
||||||
// Two short warning beeps
|
playBeep(880, 0.15)
|
||||||
await playBeep(880, 0.15)
|
|
||||||
setTimeout(() => playBeep(880, 0.15), 200)
|
setTimeout(() => playBeep(880, 0.15), 200)
|
||||||
break
|
break
|
||||||
case 'timeup':
|
case 'timeup':
|
||||||
// Descending tone sequence
|
playBeep(1200, 0.2)
|
||||||
await playBeep(1200, 0.2)
|
|
||||||
setTimeout(() => playBeep(900, 0.2), 250)
|
setTimeout(() => playBeep(900, 0.2), 250)
|
||||||
setTimeout(() => playBeep(600, 0.3), 500)
|
setTimeout(() => playBeep(600, 0.3), 500)
|
||||||
break
|
break
|
||||||
case 'meeting_end':
|
case 'meeting_end':
|
||||||
// Final chime - three notes
|
playBeep(523, 0.2)
|
||||||
await playBeep(523, 0.2) // C5
|
setTimeout(() => playBeep(659, 0.2), 200)
|
||||||
setTimeout(() => playBeep(659, 0.2), 200) // E5
|
setTimeout(() => playBeep(784, 0.4), 400)
|
||||||
setTimeout(() => playBeep(784, 0.4), 400) // G5
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMeetingStarted() {
|
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()
|
|
||||||
}
|
|
||||||
meetingActive = true
|
meetingActive = true
|
||||||
currentView = 'main'
|
currentView = 'main'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,15 +40,9 @@
|
|||||||
return audioContext
|
return audioContext
|
||||||
}
|
}
|
||||||
|
|
||||||
async function playBeep(frequency, duration, type = 'sine') {
|
function playBeep(frequency, duration, type = 'sine') {
|
||||||
try {
|
try {
|
||||||
const ctx = getAudioContext()
|
const ctx = getAudioContext()
|
||||||
// Resume context if suspended (required by browsers on first interaction)
|
|
||||||
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 oscillator = ctx.createOscillator()
|
||||||
const gainNode = ctx.createGain()
|
const gainNode = ctx.createGain()
|
||||||
|
|
||||||
@@ -58,20 +52,17 @@
|
|||||||
oscillator.frequency.value = frequency
|
oscillator.frequency.value = frequency
|
||||||
oscillator.type = type
|
oscillator.type = type
|
||||||
|
|
||||||
// Use small offset to ensure sound is scheduled in the future
|
gainNode.gain.setValueAtTime(0.3, ctx.currentTime)
|
||||||
const startTime = ctx.currentTime + 0.01
|
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + duration)
|
||||||
gainNode.gain.setValueAtTime(0.3, startTime)
|
|
||||||
gainNode.gain.exponentialRampToValueAtTime(0.01, startTime + duration)
|
|
||||||
|
|
||||||
oscillator.start(startTime)
|
oscillator.start(ctx.currentTime)
|
||||||
oscillator.stop(startTime + duration)
|
oscillator.stop(ctx.currentTime + duration)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to play sound:', e)
|
console.error('Failed to play sound:', e)
|
||||||
alert('Sound error: ' + e.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testSound(name) {
|
function testSound(name) {
|
||||||
// If custom sound exists, play it
|
// If custom sound exists, play it
|
||||||
if (customSounds[name]) {
|
if (customSounds[name]) {
|
||||||
playCustomSound(name)
|
playCustomSound(name)
|
||||||
@@ -81,16 +72,16 @@
|
|||||||
// Otherwise play default beep sounds
|
// Otherwise play default beep sounds
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'warning':
|
case 'warning':
|
||||||
await playBeep(880, 0.15)
|
playBeep(880, 0.15)
|
||||||
setTimeout(() => playBeep(880, 0.15), 200)
|
setTimeout(() => playBeep(880, 0.15), 200)
|
||||||
break
|
break
|
||||||
case 'timeup':
|
case 'timeup':
|
||||||
await playBeep(1200, 0.2)
|
playBeep(1200, 0.2)
|
||||||
setTimeout(() => playBeep(900, 0.2), 250)
|
setTimeout(() => playBeep(900, 0.2), 250)
|
||||||
setTimeout(() => playBeep(600, 0.3), 500)
|
setTimeout(() => playBeep(600, 0.3), 500)
|
||||||
break
|
break
|
||||||
case 'meeting_end':
|
case 'meeting_end':
|
||||||
await playBeep(523, 0.2)
|
playBeep(523, 0.2)
|
||||||
setTimeout(() => playBeep(659, 0.2), 200)
|
setTimeout(() => playBeep(659, 0.2), 200)
|
||||||
setTimeout(() => playBeep(784, 0.4), 400)
|
setTimeout(() => playBeep(784, 0.4), 400)
|
||||||
break
|
break
|
||||||
@@ -172,9 +163,29 @@
|
|||||||
updateComplete = true
|
updateComplete = true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Warm up AudioContext on first user interaction (for sound tests)
|
||||||
|
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.addEventListener('click', warmUpAudio)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
EventsOff('update:progress')
|
EventsOff('update:progress')
|
||||||
EventsOff('update:complete')
|
EventsOff('update:complete')
|
||||||
|
document.removeEventListener('click', warmUpAudio)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user