diff --git a/frontend/src/assets/audio/number-merge/game-over.wav b/frontend/src/assets/audio/number-merge/game-over.wav new file mode 100644 index 0000000..5d52a8c Binary files /dev/null and b/frontend/src/assets/audio/number-merge/game-over.wav differ diff --git a/frontend/src/assets/audio/number-merge/merge.wav b/frontend/src/assets/audio/number-merge/merge.wav new file mode 100644 index 0000000..2864665 Binary files /dev/null and b/frontend/src/assets/audio/number-merge/merge.wav differ diff --git a/frontend/src/assets/audio/number-merge/move.wav b/frontend/src/assets/audio/number-merge/move.wav new file mode 100644 index 0000000..12f53c8 Binary files /dev/null and b/frontend/src/assets/audio/number-merge/move.wav differ diff --git a/frontend/src/assets/audio/number-merge/win.wav b/frontend/src/assets/audio/number-merge/win.wav new file mode 100644 index 0000000..c8cc7a9 Binary files /dev/null and b/frontend/src/assets/audio/number-merge/win.wav differ diff --git a/frontend/src/features/games/number-merge/audio.ts b/frontend/src/features/games/number-merge/audio.ts index 5d193e2..23ab2f1 100644 --- a/frontend/src/features/games/number-merge/audio.ts +++ b/frontend/src/features/games/number-merge/audio.ts @@ -1,77 +1,42 @@ +import gameOverUrl from '@/assets/audio/number-merge/game-over.wav?url' +import mergeUrl from '@/assets/audio/number-merge/merge.wav?url' +import moveUrl from '@/assets/audio/number-merge/move.wav?url' +import winUrl from '@/assets/audio/number-merge/win.wav?url' + export type NumberMergeSound = 'game-over' | 'merge' | 'move' | 'win' -type Tone = { - duration: number - frequency: number - offset: number - type: OscillatorType - volume: number +const soundUrls: Record = { + 'game-over': gameOverUrl, + merge: mergeUrl, + move: moveUrl, + win: winUrl, } +const playerPools = new Map() -const sounds: Record = { - move: [{ duration: 0.065, frequency: 290, offset: 0, type: 'sine', volume: 0.085 }], - merge: [ - { duration: 0.1, frequency: 390, offset: 0, type: 'sine', volume: 0.1 }, - { duration: 0.14, frequency: 540, offset: 0.055, type: 'sine', volume: 0.12 }, - ], - win: [ - { duration: 0.16, frequency: 523.25, offset: 0, type: 'sine', volume: 0.08 }, - { duration: 0.16, frequency: 659.25, offset: 0.1, type: 'sine', volume: 0.085 }, - { duration: 0.16, frequency: 783.99, offset: 0.2, type: 'sine', volume: 0.09 }, - { duration: 0.3, frequency: 1046.5, offset: 0.3, type: 'sine', volume: 0.1 }, - ], - 'game-over': [ - { duration: 0.14, frequency: 260, offset: 0, type: 'triangle', volume: 0.065 }, - { duration: 0.18, frequency: 195, offset: 0.12, type: 'triangle', volume: 0.06 }, - { duration: 0.22, frequency: 146, offset: 0.26, type: 'triangle', volume: 0.055 }, - ], +function getPlayers(sound: NumberMergeSound): HTMLAudioElement[] { + const existing = playerPools.get(sound) + if (existing) return existing + + const players = Array.from({ length: 3 }, () => { + const player = new Audio(soundUrls[sound]) + player.preload = 'auto' + player.volume = 0.82 + return player + }) + playerPools.set(sound, players) + return players } -let audioContext: AudioContext | undefined - export function playNumberMergeSound( sound: NumberMergeSound, enabled: boolean, ): void { if (!enabled) return - const AudioContextConstructor = - window.AudioContext ?? - (window as typeof window & { webkitAudioContext?: typeof AudioContext }) - .webkitAudioContext - if (!AudioContextConstructor) { - console.error('[2048 audio] Web Audio is unavailable') - return - } - - audioContext ??= new AudioContextConstructor() - const context = audioContext - - const scheduleSound = (): void => { - const now = context.currentTime + 0.02 - for (const tone of sounds[sound]) { - const oscillator = context.createOscillator() - const gain = context.createGain() - const start = now + tone.offset - const end = start + tone.duration - - oscillator.type = tone.type - oscillator.frequency.setValueAtTime(tone.frequency, start) - gain.gain.setValueAtTime(0.0001, start) - gain.gain.linearRampToValueAtTime(tone.volume, start + 0.012) - gain.gain.exponentialRampToValueAtTime(0.0001, end) - oscillator.connect(gain).connect(context.destination) - oscillator.start(start) - oscillator.stop(end + 0.01) - } - } - - if (context.state === 'running') { - scheduleSound() - return - } - - void context.resume().then(scheduleSound).catch((error: unknown) => { - console.error('[2048 audio] Failed to start sound', error) + const players = getPlayers(sound) + const player = players.find((candidate) => candidate.paused) ?? players[0] + player.currentTime = 0 + void player.play().catch((error: unknown) => { + console.error(`[2048 audio] Failed to play ${sound}`, error) }) }