rewrite audio/spawn as single polling interval, add debug logging

Replace fragile multi-effect coordination with one effect + one
interval that polls activeRef for state transitions. No more
missed play() calls from effects not re-running.

Added console.log for: effect init/cleanup, first play, resume,
pause, and play success.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
main
name 2026-04-11 21:54:06 -07:00
parent c608e992aa
commit f939ecf3f9
1 changed files with 42 additions and 51 deletions

View File

@ -141,51 +141,70 @@ export const ArrowGame = forwardRef<ArrowGameHandle, Props>(({ active = true, ch
},
}))
// Use a ref to track active so the audio effect can read it without depending on it
// All audio/spawn logic in one effect + one interval that polls activeRef
const activeRef = useRef(active)
activeRef.current = active
const pausedAtRef = useRef(0)
// Single effect for audio + spawn lifecycle
useEffect(() => {
if (!chartNotes || !audioUrl) return
// Reset state for fresh start
// Reset
arrowsRef.current = []
setArrows([])
spawnedUpToRef.current = 0
comboRef.current = 0
setScore({ perfect: 0, good: 0, miss: 0, combo: 0 })
pausedAtRef.current = 0
const audio = new Audio(audioUrl)
audioRef.current = audio
songStartRef.current = null
let started = false
let wasActive = false
let pausedAt = 0
console.log('[ArrowGame] effect init, audioUrl:', audioUrl, 'chartNotes:', chartNotes.length)
audio.addEventListener('ended', () => onSongEndRef.current?.())
function tryPlay() {
if (started || !activeRef.current) return
audio.play().then(() => {
started = true
songStartRef.current = performance.now()
}).catch((e) => {
console.warn('Audio play failed:', e)
})
}
// Try to play immediately if already active
tryPlay()
// Spawn + progress interval
const iv = setInterval(() => {
const isActive = activeRef.current
// Handle active transitions
if (isActive && !wasActive) {
// Becoming active: play audio
if (songStartRef.current === null) {
// First play
console.log('[ArrowGame] first play attempt')
audio.play().then(() => {
songStartRef.current = performance.now()
console.log('[ArrowGame] audio playing, songStart set')
}).catch((e) => console.warn('[ArrowGame] Audio play failed:', e))
} else {
// Resume from pause
console.log('[ArrowGame] resuming from pause')
const pauseDuration = performance.now() - pausedAt
songStartRef.current += pauseDuration
for (const a of arrowsRef.current) {
a.targetTime += pauseDuration
}
audio.play().catch(() => {})
}
wasActive = true
} else if (!isActive && wasActive) {
// Becoming inactive: pause audio
console.log('[ArrowGame] pausing')
pausedAt = performance.now()
audio.pause()
wasActive = false
}
// Report progress
if (audio.duration) {
onProgressRef.current?.(audio.currentTime / audio.duration)
}
// Only spawn when song is playing
if (songStartRef.current === null) return
// Spawn arrows when playing
if (songStartRef.current === null || !isActive) return
const now = performance.now()
const songTime = (now - songStartRef.current) / 1000
@ -209,6 +228,7 @@ export const ArrowGame = forwardRef<ArrowGameHandle, Props>(({ active = true, ch
}, 50)
return () => {
console.log('[ArrowGame] effect cleanup')
clearInterval(iv)
audio.pause()
audio.src = ''
@ -217,35 +237,6 @@ export const ArrowGame = forwardRef<ArrowGameHandle, Props>(({ active = true, ch
}
}, [chartNotes, audioUrl])
// Pause/resume audio when active changes
useEffect(() => {
const audio = audioRef.current
if (!audio) return
if (active) {
if (songStartRef.current === null) {
// First play — audio was created but not started yet
audio.play().then(() => {
songStartRef.current = performance.now()
}).catch((e) => {
console.warn('Audio play failed:', e)
})
} else if (pausedAtRef.current > 0) {
// Resuming from pause
const pauseDuration = performance.now() - pausedAtRef.current
songStartRef.current += pauseDuration
for (const a of arrowsRef.current) {
a.targetTime += pauseDuration
}
pausedAtRef.current = 0
audio.play().catch(() => {})
}
} else if (songStartRef.current !== null) {
pausedAtRef.current = performance.now()
audio.pause()
}
}, [active])
// Miss detection + cleanup
useEffect(() => {
if (!active) return