Skip to content

Commit 385936c

Browse files
authored
feat(go): add useAudio hook (#974)
This fixes a bug where the audiocontext gets suspended by iOS when the document.visibilityState changes. See goldfire/howler.js#1660 goldfire/howler.js#1770 goldfire/howler.js#1702 goldfire/howler.js#1771 Good to note this is not a Howler bug, it's an iOS bug. We have tried several audio libraries that all produced the same behaviour.
1 parent 6b047bb commit 385936c

File tree

2 files changed

+54
-8
lines changed

2 files changed

+54
-8
lines changed

kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/components/SoundEffects.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
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 placeSoundUrl from '../assets/audio/go-stone-placing.mp3';
66
import captureSoundUrl from '../assets/audio/go-stone-removal.mp3';
77

8-
const placeSound = new Howl({ src: [placeSoundUrl], volume: 0.5 });
9-
const captureSound = new Howl({ src: [captureSoundUrl], volume: 0.5 });
10-
118
const THROTTLE_MS = 150;
129

1310
export default function SoundEffects() {
1411
const game = useGameStore((state) => state.game);
1512
const soundEnabled = usePreferences((state) => state.soundEnabled);
13+
const sounds = useAudio({ place: placeSoundUrl, capture: captureSoundUrl });
1614
const prevRef = useRef({ move: 0, captures: 0 });
1715
const lastPlayedRef = useRef(0);
1816

@@ -29,14 +27,13 @@ export default function SoundEffects() {
2927

3028
if (!soundEnabled || (!placed && !captured)) return;
3129

32-
// Prevent audio spam when scrubbing quickly (e.g. holding arrow keys)
3330
const now = performance.now();
3431
if (now - lastPlayedRef.current < THROTTLE_MS) return;
3532
lastPlayedRef.current = now;
3633

37-
if (placed) placeSound.play();
38-
if (captured) captureSound.play();
39-
}, [game, soundEnabled]);
34+
if (placed) sounds.place.play();
35+
if (captured) sounds.capture.play();
36+
}, [game, soundEnabled, sounds]);
4037

4138
return null;
4239
}
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)