Skip to content

Commit 8d205bb

Browse files
authored
feat(chess): use audio hook (#979)
Similar to #974
1 parent 5dedc3b commit 8d205bb

File tree

2 files changed

+54
-8
lines changed

2 files changed

+54
-8
lines changed
Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import { useEffect, useRef } from 'react';
2-
import { Howl } from 'howler';
32
import useGameStore from '../stores/useGameStore';
43
import usePreferences from '../stores/usePreferences';
4+
import useAudio from '../hooks/useAudio';
55
import moveSoundUrl from '../assets/audio/chess-move.mp3';
66
import captureSoundUrl from '../assets/audio/chess-capture.mp3';
77
import { SCRUB_THRESHOLD_MS } from '../constants';
88

9-
const placeSound = new Howl({ src: [moveSoundUrl], volume: 0.5 });
10-
const captureSound = new Howl({ src: [captureSoundUrl], volume: 0.5 });
11-
129
export function SoundEffects() {
1310
const { game, options } = useGameStore();
1411
const soundEnabled = usePreferences((state) => state.soundEnabled);
12+
const sounds = useAudio({ move: moveSoundUrl, capture: captureSoundUrl });
1513
const lastPlayedRef = useRef(0);
1614
const lastStep = useRef(0);
1715

@@ -25,14 +23,13 @@ export function SoundEffects() {
2523
const currentMove = history.at(-1);
2624
const captured = currentMove?.isCapture();
2725

28-
// Prevent audio spam when scrubbing quickly (e.g. holding arrow keys)
2926
const now = performance.now();
3027
if (now - lastPlayedRef.current < SCRUB_THRESHOLD_MS) return;
3128
lastPlayedRef.current = now;
3229

33-
placeSound.play();
34-
if (captured) captureSound.play();
35-
}, [game, options.step, soundEnabled]);
30+
sounds.move.play();
31+
if (captured) sounds.capture.play();
32+
}, [game, options.step, sounds, soundEnabled]);
3633

3734
return null;
3835
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { Howl, Howler } from 'howler';
3+
4+
type SoundConfig = { src: string; volume?: number };
5+
type SoundMap<T extends string> = Record<T, SoundConfig | string>;
6+
type Sounds<T extends string> = Record<T, Howl>;
7+
8+
function createSounds<T extends string>(map: SoundMap<T>): Sounds<T> {
9+
const entries = Object.entries<SoundConfig | string>(map).map(([key, value]) => {
10+
const config = typeof value === 'string' ? { src: value } : value;
11+
return [key, new Howl({ src: [config.src], volume: config.volume ?? 0.5 })];
12+
});
13+
return Object.fromEntries(entries) as Sounds<T>;
14+
}
15+
16+
export default function useAudio<T extends string>(map: SoundMap<T>): Sounds<T> {
17+
const mapRef = useRef(map);
18+
const [sounds, setSounds] = useState(() => createSounds(map));
19+
20+
useEffect(() => {
21+
function cleanup() {
22+
window.removeEventListener('pointerdown', resume);
23+
window.removeEventListener('keydown', resume);
24+
}
25+
26+
function resume() {
27+
Howler.unload();
28+
setSounds(createSounds(mapRef.current));
29+
cleanup();
30+
}
31+
32+
const handleVisibilityChange = () => {
33+
if (document.visibilityState !== 'visible') return;
34+
const state = Howler.ctx?.state as string;
35+
if (state !== 'interrupted' && state !== 'suspended') return;
36+
37+
window.addEventListener('pointerdown', resume);
38+
window.addEventListener('keydown', resume);
39+
};
40+
41+
document.addEventListener('visibilitychange', handleVisibilityChange);
42+
return () => {
43+
document.removeEventListener('visibilitychange', handleVisibilityChange);
44+
cleanup();
45+
};
46+
}, []);
47+
48+
return sounds;
49+
}

0 commit comments

Comments
 (0)