Skip to content

Commit 040af4e

Browse files
Merge pull request #915 from HardyNLee/fix/resume-audio-context
fix: resume audio context
2 parents 5e902ea + 95a62f1 commit 040af4e

2 files changed

Lines changed: 99 additions & 59 deletions

File tree

packages/webgal/src/Core/gameScripts/vocal/index.ts

Lines changed: 63 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import { getBooleanArgByKey, getNumberArgByKey, getStringArgByKey } from '@/Core
66
import { IStageState } from '@/store/stageInterface';
77
import {
88
audioContextWrapper,
9+
ensureAudioContextReady,
910
getAudioLevel,
1011
performBlinkAnimation,
1112
performMouthAnimation,
13+
resetMaxAudioLevel,
1214
updateThresholds,
1315
} from '@/Core/gameScripts/vocal/vocalAnimation';
1416
import { match } from '../../util/match';
@@ -64,7 +66,7 @@ export const playVocal = (sentence: ISentence) => {
6466
return {
6567
arrangePerformPromise: new Promise((resolve) => {
6668
// 播放语音
67-
setTimeout(() => {
69+
setTimeout(async () => {
6870
let VocalControl: any = document.getElementById('currentVocal');
6971
// 设置语音音量
7072
webgalStore.dispatch(setStage({ key: 'vocalVolume', value: volume }));
@@ -102,64 +104,71 @@ export const playVocal = (sentence: ISentence) => {
102104
stopTimeout: undefined, // 暂时不用,后面会交给自动清除
103105
};
104106
WebGAL.gameplay.performController.arrangeNewPerform(perform, sentence, false);
107+
const finishPerform = () => {
108+
for (const e of WebGAL.gameplay.performController.performList) {
109+
if (e.performName === performInitName) {
110+
isOver = true;
111+
e.stopFunction();
112+
WebGAL.gameplay.performController.unmountPerform(e.performName);
113+
}
114+
}
115+
};
116+
105117
key = key ? key : `fig-${pos}`;
106118
const animationItem = figureAssociatedAnimation.find((tid) => tid.targetId === key);
107119
if (animationItem) {
108-
let maxAudioLevel = 0;
120+
resetMaxAudioLevel();
109121

110122
const foundFigure = freeFigure.find((figure) => figure.key === key);
111123

112124
if (foundFigure) {
113125
pos = foundFigure.basePosition;
114126
}
115127

116-
if (!audioContextWrapper.audioContext) {
117-
let audioContext: AudioContext | null;
118-
audioContext = new AudioContext();
119-
audioContextWrapper.analyser = audioContext.createAnalyser();
120-
audioContextWrapper.analyser.fftSize = 256;
121-
audioContextWrapper.dataArray = new Uint8Array(audioContextWrapper.analyser.frequencyBinCount);
122-
}
123-
124-
if (!audioContextWrapper.analyser) {
125-
audioContextWrapper.analyser = audioContextWrapper.audioContext.createAnalyser();
126-
audioContextWrapper.analyser.fftSize = 256;
127-
}
128-
129-
bufferLength = audioContextWrapper.analyser.frequencyBinCount;
130-
audioContextWrapper.dataArray = new Uint8Array(bufferLength);
131-
let vocalControl = document.getElementById('currentVocal') as HTMLMediaElement;
132-
133-
if (!audioContextWrapper.source || audioContextWrapper.source.mediaElement !== vocalControl) {
134-
if (audioContextWrapper.source) {
135-
audioContextWrapper.source.disconnect();
128+
const isAudioContextReady = await ensureAudioContextReady();
129+
if (isAudioContextReady && audioContextWrapper.audioContext) {
130+
if (!audioContextWrapper.analyser) {
131+
audioContextWrapper.analyser = audioContextWrapper.audioContext.createAnalyser();
132+
audioContextWrapper.analyser.fftSize = 256;
136133
}
137-
audioContextWrapper.source = audioContextWrapper.audioContext.createMediaElementSource(vocalControl);
138-
audioContextWrapper.source.connect(audioContextWrapper.analyser!);
139-
}
140134

141-
audioContextWrapper.analyser.connect(audioContextWrapper.audioContext.destination);
135+
bufferLength = audioContextWrapper.analyser.frequencyBinCount;
136+
audioContextWrapper.dataArray = new Uint8Array(bufferLength);
137+
let vocalControl = document.getElementById('currentVocal') as HTMLMediaElement;
142138

143-
// Lip-snc Animation
144-
audioContextWrapper.audioLevelInterval = setInterval(() => {
145-
const audioLevel = getAudioLevel(
146-
audioContextWrapper.analyser!,
147-
audioContextWrapper.dataArray!,
148-
bufferLength,
149-
);
150-
const { OPEN_THRESHOLD, HALF_OPEN_THRESHOLD } = updateThresholds(audioLevel);
139+
if (!audioContextWrapper.source || audioContextWrapper.source.mediaElement !== vocalControl) {
140+
if (audioContextWrapper.source) {
141+
audioContextWrapper.source.disconnect();
142+
}
143+
audioContextWrapper.source = audioContextWrapper.audioContext.createMediaElementSource(vocalControl);
144+
audioContextWrapper.source.connect(audioContextWrapper.analyser);
145+
}
151146

152-
performMouthAnimation({
153-
audioLevel,
154-
OPEN_THRESHOLD,
155-
HALF_OPEN_THRESHOLD,
156-
currentMouthValue,
157-
lerpSpeed,
158-
key,
159-
animationItem,
160-
pos,
161-
});
162-
}, 50);
147+
audioContextWrapper.analyser.connect(audioContextWrapper.audioContext.destination);
148+
149+
// Lip-sync Animation
150+
audioContextWrapper.audioLevelInterval = setInterval(() => {
151+
const audioLevel = getAudioLevel(
152+
audioContextWrapper.analyser!,
153+
audioContextWrapper.dataArray!,
154+
bufferLength,
155+
);
156+
const { OPEN_THRESHOLD, HALF_OPEN_THRESHOLD } = updateThresholds(audioLevel);
157+
158+
performMouthAnimation({
159+
audioLevel,
160+
OPEN_THRESHOLD,
161+
HALF_OPEN_THRESHOLD,
162+
currentMouthValue,
163+
lerpSpeed,
164+
key,
165+
animationItem,
166+
pos,
167+
});
168+
}, 50);
169+
} else {
170+
logger.warn('AudioContext is not ready, skip lip-sync analyzer for this vocal.');
171+
}
163172

164173
// blinkAnimation
165174
let animationEndTime: number;
@@ -174,17 +183,16 @@ export const playVocal = (sentence: ISentence) => {
174183
}, 10000);
175184
}
176185

177-
VocalControl?.play();
186+
const playPromise = VocalControl?.play();
178187

179-
VocalControl.onended = () => {
180-
for (const e of WebGAL.gameplay.performController.performList) {
181-
if (e.performName === performInitName) {
182-
isOver = true;
183-
e.stopFunction();
184-
WebGAL.gameplay.performController.unmountPerform(e.performName);
185-
}
186-
}
187-
};
188+
if (playPromise?.catch) {
189+
playPromise.catch((error: unknown) => {
190+
logger.warn('Vocal play was blocked by browser autoplay policy or audio activation state.', error);
191+
finishPerform();
192+
});
193+
}
194+
195+
VocalControl.onended = finishPerform;
188196
}
189197
}, 1);
190198
}),

packages/webgal/src/Core/gameScripts/vocal/vocalAnimation.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { WebGAL } from '@/Core/WebGAL';
22

33
interface IAudioContextWrapper {
4-
audioContext: AudioContext;
4+
audioContext: AudioContext | null;
55
source: MediaElementAudioSourceNode | null;
66
analyser: AnalyserNode | undefined;
77
dataArray: Uint8Array | undefined;
@@ -12,7 +12,7 @@ interface IAudioContextWrapper {
1212

1313
// Initialize the object based on the interface
1414
export const audioContextWrapper: IAudioContextWrapper = {
15-
audioContext: new AudioContext(),
15+
audioContext: null,
1616
source: null,
1717
analyser: undefined,
1818
dataArray: undefined,
@@ -21,6 +21,34 @@ export const audioContextWrapper: IAudioContextWrapper = {
2121
maxAudioLevel: 0,
2222
};
2323

24+
export const ensureAudioContextReady = async (): Promise<boolean> => {
25+
if (!audioContextWrapper.audioContext) {
26+
const AudioContextCtor =
27+
window.AudioContext ??
28+
(window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
29+
30+
if (!AudioContextCtor) {
31+
return false;
32+
}
33+
34+
audioContextWrapper.audioContext = new AudioContextCtor();
35+
}
36+
37+
if (audioContextWrapper.audioContext.state === 'suspended') {
38+
try {
39+
await audioContextWrapper.audioContext.resume();
40+
} catch {
41+
return false;
42+
}
43+
}
44+
45+
return audioContextWrapper.audioContext.state === 'running';
46+
};
47+
48+
export const resetMaxAudioLevel = () => {
49+
audioContextWrapper.maxAudioLevel = 0;
50+
};
51+
2452
export const updateThresholds = (audioLevel: number) => {
2553
audioContextWrapper.maxAudioLevel = Math.max(audioLevel, audioContextWrapper.maxAudioLevel);
2654
return {
@@ -52,8 +80,12 @@ export const performBlinkAnimation = (params: {
5280
};
5381

5482
// Updated getAudioLevel function
55-
export const getAudioLevel = (analyser: AnalyserNode, dataArray: Uint8Array, bufferLength: number): number => {
56-
analyser.getByteFrequencyData(dataArray);
83+
export const getAudioLevel = (
84+
analyser: AnalyserNode,
85+
dataArray: Uint8Array,
86+
bufferLength: number,
87+
): number => {
88+
analyser.getByteFrequencyData(dataArray as any);
5789
let sum = 0;
5890
for (let i = 0; i < bufferLength; i++) {
5991
sum += dataArray[i];

0 commit comments

Comments
 (0)