diff --git a/src/assets/icq-uh-oh.wav b/src/assets/icq-uh-oh.wav new file mode 100644 index 0000000..7ad9f21 Binary files /dev/null and b/src/assets/icq-uh-oh.wav differ diff --git a/src/components/GamePage.vue b/src/components/GamePage.vue index bb048d9..53a2bfa 100644 --- a/src/components/GamePage.vue +++ b/src/components/GamePage.vue @@ -44,6 +44,7 @@ import type { TeamResult, Ticket, } from '@/api/generated/crabs/evening_detective_server' +import notificationSoundURL from '@/assets/icq-uh-oh.wav' import GameMapPanel from '@/components/GameMapPanel.vue' import GameScreensPanel from '@/components/GameScreensPanel.vue' import GameStatePanel from '@/components/GameStatePanel.vue' @@ -134,6 +135,7 @@ let appliedGameRequestVersion = 0 let hasLoadedGame = false let isDisposed = false let notificationAudio: AudioContext | undefined +let notificationSound: Promise | undefined let hasReportedSoundError = false const canViewState = computed( () => @@ -239,30 +241,29 @@ async function enableNotificationSound() { try { notificationAudio ??= new AudioContext() if (notificationAudio.state === 'suspended') await notificationAudio.resume() + const audio = notificationAudio + notificationSound ??= fetch(notificationSoundURL).then(async (response) => { + if (!response.ok) throw new Error('Не удалось загрузить звук уведомления') + return audio.decodeAudioData(await response.arrayBuffer()) + }) + await notificationSound } catch (error) { + notificationSound = undefined reportSoundError(error) } } -function playApplicationNotification() { +async function playApplicationNotification() { // До первого взаимодействия браузер блокирует звук; пропущенные сигналы не накапливаем. - if (!notificationAudio || notificationAudio.state !== 'running') return + if (!notificationAudio || notificationAudio.state !== 'running' || !notificationSound) return try { - const oscillator = notificationAudio.createOscillator() - const gain = notificationAudio.createGain() - const now = notificationAudio.currentTime - oscillator.frequency.value = 880 - gain.gain.setValueAtTime(0, now) - gain.gain.linearRampToValueAtTime(0.1, now + 0.02) - gain.gain.linearRampToValueAtTime(0, now + 0.3) - oscillator.connect(gain) - gain.connect(notificationAudio.destination) - oscillator.onended = () => { - oscillator.disconnect() - gain.disconnect() - } - oscillator.start(now) - oscillator.stop(now + 0.3) + const buffer = await notificationSound + if (isDisposed || notificationAudio.state !== 'running') return + const source = notificationAudio.createBufferSource() + source.buffer = buffer + source.connect(notificationAudio.destination) + source.onended = () => source.disconnect() + source.start() } catch (error) { reportSoundError(error) } @@ -270,7 +271,7 @@ function playApplicationNotification() { async function testNotificationSound() { await enableNotificationSound() - playApplicationNotification() + await playApplicationNotification() } onMounted(() => { @@ -303,7 +304,7 @@ async function getGame(id: number) { hasLoadedGame && hasNewTeamApplications(game.value.teams || [], res.game?.teams || []) ) { - playApplicationNotification() + void playApplicationNotification() } hasLoadedGame = true game.value = res.game!