Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/webgal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"axios": "^0.30.2",
"cloudlogjs": "^1.0.9",
"gifuct-js": "^2.1.2",
"gsap": "^3.14.2",
"i18next": "^22.4.15",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
Expand Down
227 changes: 227 additions & 0 deletions packages/webgal/src/Core/Modules/audio/bgmManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import gsap from 'gsap';

class BgmManager {
private static instance: BgmManager;

public static getInstance(): BgmManager {
if (!BgmManager.instance) {
BgmManager.instance = new BgmManager();
}
return BgmManager.instance;
}

private _audios: [HTMLAudioElement, HTMLAudioElement];
private _currentIndex = 0;
private _targetVolume = 1;
private _loop = true;
private _muted = false;
private _progressListeners: Set<(p: { currentTime: number; duration: number }) => void> = new Set();

private constructor() {
this._audios = [new Audio(), new Audio()];
this._audios.forEach((audio) => {
audio.loop = this._loop;
audio.preload = 'auto';
audio.crossOrigin = 'anonymous';
audio.addEventListener('timeupdate', this._onTimeUpdate);
});
}

public async play(options: { src?: string; loop?: boolean; volume?: number; fade?: number } = {}): Promise<void> {
const fade = options.fade ?? 0;
if (options.volume !== undefined) this._targetVolume = options.volume;
if (options.loop !== undefined) this.loop = options.loop;
Comment on lines +31 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

当前 play 方法在被调用时,即使传入的 src 与当前播放的音轨相同,也会重新加载并播放音频,这会导致音乐从头开始。在某些场景下(例如,仅音量变化时),这可能不是预期的行为。

建议在 play 方法的开头增加一个判断:如果请求的 src 与当前正在播放的音轨相同且并未暂停,那么应该只调整音量,而不是重启整个音轨。这能让 play 方法在重复调用时表现更符合预期,尤其是在 React useEffect 中使用时。

    const fade = options.fade ?? 0;
    if (options.volume !== undefined) this._targetVolume = options.volume;
    if (options.loop !== undefined) this.loop = options.loop;

    // If src is provided and is the same as the current one, just adjust volume
    if (options.src && options.src === this._audio.src && !this._audio.paused) {
      await this._setVolume({ index: this._currentIndex, volume: this._targetVolume, fade });
      return;
    }


if (!options.src) {
const current = this._audio;
if (current.src) {
if (current.paused) {
current.volume = 0;
await current.play();
}
await this._setVolume({ index: this._currentIndex, volume: this._targetVolume, fade });
}
return;
}

const oldIndex = this._currentIndex;
const nextIndex = (this._currentIndex + 1) % 2;
const oldAudio = this._audios[oldIndex];
const nextAudio = this._audios[nextIndex];

nextAudio.src = options.src;
nextAudio.volume = fade > 0 ? 0 : this._targetVolume;
nextAudio.muted = this._muted;

try {
nextAudio.load();
await new Promise((resolve, reject) => {
const onCanPlay = () => {
nextAudio.removeEventListener('error', onError);
resolve(null);
};
const onError = (e: any) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

为了增强代码的类型安全性和可读性,建议为 onError 回调函数中的事件参数 e 指定更明确的类型 Event,而不是使用 any

Suggested change
const onError = (e: any) => {
const onError = (e: Event) => {

nextAudio.removeEventListener('canplaythrough', onCanPlay);
reject(e);
};
nextAudio.addEventListener('canplaythrough', onCanPlay, { once: true });
nextAudio.addEventListener('error', onError, { once: true });
});

await nextAudio.play();
this._currentIndex = nextIndex;

if (fade > 0) {
await Promise.all([
this._setVolume({ index: oldIndex, volume: 0, fade, stopOnEnd: true }),
this._setVolume({ index: nextIndex, volume: this._targetVolume, fade }),
]);
} else {
this._stopAudio(oldAudio);
}
} catch (e) {
console.error('BGM Playback failed:', e);
this._stopAudio(nextAudio);
}
}

public async pause({ fade = 0 }: { fade?: number }): Promise<void> {
if (fade > 0) {
await this._setVolume({ index: this._currentIndex, volume: 0, fade, pauseOnEnd: true });
} else {
this._audio.pause();
}
}

public async stop({ fade = 0 }: { fade?: number }): Promise<void> {
if (fade > 0) {
await this._setVolume({ index: this._currentIndex, volume: 0, fade, stopOnEnd: true });
} else {
this._audios.forEach((_, i) => this._stopAudio(this._audios[i]));
}
}

public async fade({ volume, fade = 0 }: { volume: number; fade?: number }): Promise<void> {
this._targetVolume = volume;
return this._setVolume({ index: this._currentIndex, volume, fade });
}

public async resume({ fade = 0 }: { fade?: number }): Promise<void> {
return this.play({ fade });
}

public addProgressListener(cb: (p: { currentTime: number; duration: number }) => void): () => void {
this._progressListeners.add(cb);

return () => {
this._progressListeners.delete(cb);
};
}

public clearListeners(): void {
this._progressListeners.clear();
}

private get _audio() {
return this._audios[this._currentIndex];
}

public get currentTime() {
return this._audio.currentTime;
}
public set currentTime(value: number) {
this._audio.currentTime = value;
}

public get duration() {
return this._audio.duration;
}
public get paused() {
return this._audio.paused;
}

public get volume() {
return this._targetVolume;
}
public set volume(value: number) {
this._targetVolume = value;
gsap.killTweensOf(this._audio, 'volume');
this._audio.volume = Math.max(0, Math.min(1, value));
}

public get loop() {
return this._loop;
}
public set loop(value: boolean) {
this._loop = value;
this._audios.forEach((a) => {
a.loop = value;
});
}

public get muted() {
return this._muted;
}
public set muted(value: boolean) {
this._muted = value;
this._audios.forEach((a) => {
a.muted = value;
});
}

private _setVolume(params: {
index: number;
volume: number;
fade: number;
stopOnEnd?: boolean;
pauseOnEnd?: boolean;
}): Promise<void> {
const { index, volume, fade, stopOnEnd, pauseOnEnd } = params;

const audio = this._audios[index];

if (!audio.src || audio.src === window.location.href) {
return Promise.resolve();
}

gsap.killTweensOf(audio, 'volume');

return new Promise((resolve) => {
if (fade <= 0) {
audio.volume = volume;
if (stopOnEnd) this._stopAudio(audio);
else if (pauseOnEnd) audio.pause();
resolve();
return;
}

gsap.to(audio, {
volume,
duration: fade / 1000,
ease: volume > audio.volume ? 'sine.out' : 'sine.in',
overwrite: 'auto',
onComplete: () => {
if (stopOnEnd) this._stopAudio(audio);
else if (pauseOnEnd) audio.pause();
resolve();
},
onInterrupt: () => resolve(),
});
});
}

private _onTimeUpdate = () => {
if (!this._audio.src || this._progressListeners.size === 0) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

_onTimeUpdate 方法中,使用 !this._audio.src 来判断是否加载了音频源可能不够可靠。当通过 removeAttribute('src') 清除音源后,src 属性可能会指向当前页面的 URL。建议使用 this._audio.currentSrc 进行判断,当没有媒体加载时,它会是一个空字符串,这样更健壮。

Suggested change
if (!this._audio.src || this._progressListeners.size === 0) return;
if (!this._audio.currentSrc || this._progressListeners.size === 0) return;

const { currentTime, duration } = this._audio;
this._progressListeners.forEach((listener) => listener({ currentTime, duration }));
};

private _stopAudio(audio: HTMLAudioElement) {
gsap.killTweensOf(audio, 'volume');
audio.pause();
audio.removeAttribute('src');
audio.load();
}
}

export const bgmManager = BgmManager.getInstance();
32 changes: 3 additions & 29 deletions packages/webgal/src/Core/controller/stage/playBgm.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,7 @@
import { webgalStore } from '@/store/store';
import { setStage } from '@/store/stageReducer';
import { logger } from '@/Core/util/logger';

// /**
// * 停止bgm
// */
// export const eraseBgm = () => {
// logger.debug(`停止bgm`);
// // 停止之前的bgm
// let VocalControl: any = document.getElementById('currentBgm');
// if (VocalControl !== null) {
// VocalControl.currentTime = 0;
// if (!VocalControl.paused) VocalControl.pause();
// }
// // 获得舞台状态并设置
// webgalStore.dispatch(setStage({key: 'bgm', value: ''}));
// };

let emptyBgmTimeout: ReturnType<typeof setTimeout>;
import { bgmManager } from '@/Core/Modules/audio/bgmManager';

/**
* 播放bgm
Expand All @@ -28,21 +12,11 @@ let emptyBgmTimeout: ReturnType<typeof setTimeout>;
export function playBgm(url: string, enter = 0, volume = 100): void {
logger.debug('playing bgm' + url);
if (url === '') {
emptyBgmTimeout = setTimeout(() => {
// 淡入淡出效果结束后,将 bgm 置空
webgalStore.dispatch(setStage({ key: 'bgm', value: { src: '', enter: 0, volume: 100 } }));
}, enter);
bgmManager.stop({ fade: enter });
const lastSrc = webgalStore.getState().stage.bgm.src;
webgalStore.dispatch(setStage({ key: 'bgm', value: { src: lastSrc, enter: -enter, volume: volume } }));
} else {
// 不要清除bgm了!
clearTimeout(emptyBgmTimeout);
webgalStore.dispatch(setStage({ key: 'bgm', value: { src: url, enter: enter, volume: volume } }));
}
setTimeout(() => {
const audioElement = document.getElementById('currentBgm') as HTMLAudioElement;
if (audioElement.src) {
audioElement?.play();
}
}, 0);
bgmManager.play({ src: url, volume: volume / 100, fade: enter });
}
21 changes: 0 additions & 21 deletions packages/webgal/src/Core/controller/stage/setVolume.ts

This file was deleted.

19 changes: 8 additions & 11 deletions packages/webgal/src/Core/gameScripts/playVideo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import { webgalStore } from '@/store/store';
import { getRandomPerformName, PerformController } from '@/Core/Modules/perform/performController';
import { getBooleanArgByKey } from '@/Core/util/getSentenceArg';
import { WebGAL } from '@/Core/WebGAL';
import { bgmManager } from '../Modules/audio/bgmManager';
/**
* 播放一段视频 * @param sentence
*/
export const playVideo = (sentence: ISentence): IPerform => {
const stageState = webgalStore.getState().stage;
const userDataState = webgalStore.getState().userData;
const mainVol = userDataState.optionData.volumeMain;
const vocalVol = mainVol * 0.01 * userDataState.optionData.vocalVolume * 0.01;
const bgmVol = mainVol * 0.01 * userDataState.optionData.bgmVolume * 0.01;
const bgmEnter = stageState.bgm.enter;
const performInitName: string = getRandomPerformName();

let blockingNextFlag = getBooleanArgByKey(sentence, 'skipOff') ?? false;
Expand All @@ -31,7 +34,7 @@ export const playVideo = (sentence: ISentence): IPerform => {
performName: 'none',
duration: 0,
isHoldOn: false,
stopFunction: () => {},
stopFunction: () => { },
blockingNext: () => blockingNextFlag,
blockingAuto: () => true,
stopTimeout: undefined, // 暂时不用,后面会交给自动清除
Expand Down Expand Up @@ -69,12 +72,9 @@ export const playVideo = (sentence: ISentence): IPerform => {
/**
* 恢复音量
*/
const bgmElement: any = document.getElementById('currentBgm');
if (bgmElement) {
bgmElement.volume = bgmVol.toString();
}
bgmManager.resume({ fade: bgmEnter });
const vocalElement: any = document.getElementById('currentVocal');
if (bgmElement) {
if (vocalElement) {
vocalElement.volume = vocalVol.toString();
}
// eslint-disable-next-line react/no-deprecated
Expand All @@ -93,12 +93,9 @@ export const playVideo = (sentence: ISentence): IPerform => {
*/
const vocalVol2 = 0;
const bgmVol2 = 0;
const bgmElement: any = document.getElementById('currentBgm');
if (bgmElement) {
bgmElement.volume = bgmVol2.toString();
}
bgmManager.pause({ fade: bgmEnter });
const vocalElement: any = document.getElementById('currentVocal');
if (bgmElement) {
if (vocalElement) {
vocalElement.volume = vocalVol2.toString();
}

Expand Down
Loading
Loading