From 5a68ffabb21ad1742def784e425994e5b31e952f Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Sat, 14 Mar 2026 10:30:37 +0800 Subject: [PATCH 1/3] feat: new bgm manager --- packages/webgal/package.json | 1 + .../src/Core/Modules/audio/bgmManager.ts | 118 ++++++++++++++++++ .../src/Core/controller/stage/playBgm.ts | 32 +---- .../src/Core/controller/stage/setVolume.ts | 21 ---- .../webgal/src/Core/gameScripts/playVideo.tsx | 17 +-- .../Stage/AudioContainer/AudioContainer.tsx | 63 ++-------- packages/webgal/src/UI/Extra/ExtraBgm.tsx | 12 +- packages/webgal/src/UI/Title/Title.tsx | 8 +- yarn.lock | 5 + 9 files changed, 151 insertions(+), 126 deletions(-) create mode 100644 packages/webgal/src/Core/Modules/audio/bgmManager.ts delete mode 100644 packages/webgal/src/Core/controller/stage/setVolume.ts diff --git a/packages/webgal/package.json b/packages/webgal/package.json index 892349f77..9802cb179 100644 --- a/packages/webgal/package.json +++ b/packages/webgal/package.json @@ -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", diff --git a/packages/webgal/src/Core/Modules/audio/bgmManager.ts b/packages/webgal/src/Core/Modules/audio/bgmManager.ts new file mode 100644 index 000000000..26d95719a --- /dev/null +++ b/packages/webgal/src/Core/Modules/audio/bgmManager.ts @@ -0,0 +1,118 @@ +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 activeIndex = 0; + private progressListeners: Set<(p: { current: number; total: number; ratio: number }) => void> = new Set(); + + private constructor() { + this.audios = [new Audio(), new Audio()]; + this.audios.forEach((audio, index) => { + audio.loop = true; + audio.crossOrigin = 'anonymous'; + audio.addEventListener('timeupdate', () => this.handleTimeUpdate(index)); + }); + } + + public async play(src: string, options: { volume: number; fade: number }) { + const nextIndex = (this.activeIndex + 1) % 2; + const currentAudio = this.audios[this.activeIndex]; + + if (currentAudio.src === src) { + if (currentAudio.paused) { + await this.resume(options); + } + return; + } + + const nextAudio = this.audios[nextIndex]; + + nextAudio.src = src; + nextAudio.volume = 0; + + try { + await nextAudio.play(); + + this.activeIndex = nextIndex; + + gsap.killTweensOf(currentAudio, 'volume'); + gsap.to(currentAudio, { + volume: 0, + duration: options.fade / 1000, + ease: 'sine.in', + onComplete: () => currentAudio.pause(), + }); + + gsap.killTweensOf(nextAudio, 'volume'); + gsap.to(nextAudio, { + volume: options.volume, + duration: options.fade / 1000, + ease: 'sine.out', + }); + } catch (err) { + console.error('BGM Playback failed:', err); + } + } + + public async pause(options: { fade: number }) { + const currentAudio = this.audios[this.activeIndex]; + gsap.killTweensOf(currentAudio, 'volume'); + await gsap.to(currentAudio, { + volume: 0, + duration: options.fade / 1000, + ease: 'sine.in', + onComplete: () => currentAudio.pause(), + }); + } + + public async resume(options: { volume: number; fade: number }) { + const currentAudio = this.audios[this.activeIndex]; + gsap.killTweensOf(currentAudio, 'volume'); + await gsap.to(currentAudio, { + volume: options.volume, + duration: options.fade / 1000, + ease: 'sine.out', + }); + } + + public async stop(options: { fade: number }) { + const currentAudio = this.audios[this.activeIndex]; + gsap.killTweensOf(currentAudio, 'volume'); + await gsap.to(currentAudio, { + volume: 0, + duration: options.fade / 1000, + ease: 'sine.in', + onComplete: () => { + currentAudio.pause(); + currentAudio.currentTime = 0; + }, + }); + } + + public subscribeProgress(cb: (p: any) => void) { + this.progressListeners.add(cb); + return () => this.progressListeners.delete(cb); + } + + private handleTimeUpdate(index: number) { + if (index !== this.activeIndex) return; + const audio = this.audios[index]; + const data = { + current: audio.currentTime, + total: audio.duration || 0, + ratio: audio.currentTime / (audio.duration || 1), + }; + this.progressListeners.forEach((cb) => cb(data)); + } +} + +export const bgmManager = BgmManager.getInstance(); diff --git a/packages/webgal/src/Core/controller/stage/playBgm.ts b/packages/webgal/src/Core/controller/stage/playBgm.ts index b6e76c2e8..6f7f82c5c 100644 --- a/packages/webgal/src/Core/controller/stage/playBgm.ts +++ b/packages/webgal/src/Core/controller/stage/playBgm.ts @@ -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; +import { bgmManager } from '@/Core/Modules/audio/bgmManager'; /** * 播放bgm @@ -28,21 +12,11 @@ let emptyBgmTimeout: ReturnType; 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(url, { volume: volume / 100, fade: enter }); } diff --git a/packages/webgal/src/Core/controller/stage/setVolume.ts b/packages/webgal/src/Core/controller/stage/setVolume.ts deleted file mode 100644 index 14f0ec36f..000000000 --- a/packages/webgal/src/Core/controller/stage/setVolume.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { logger } from '../../util/logger'; -import { webgalStore } from '@/store/store'; - -/** - * 设置音量 - */ -export const setVolume = () => { - 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; - logger.debug(`设置背景音量:${bgmVol},语音音量:${vocalVol}`); - // const bgmElement: any = document.getElementById('currentBgm'); - // if (bgmElement) { - // bgmElement.volume = bgmVol.toString(); - // } - // const vocalElement: any = document.getElementById('currentVocal'); - // if (vocalElement) { - // vocalElement.volume = vocalVol.toString(); - // } -}; diff --git a/packages/webgal/src/Core/gameScripts/playVideo.tsx b/packages/webgal/src/Core/gameScripts/playVideo.tsx index d3dbd4e69..a8a4430da 100644 --- a/packages/webgal/src/Core/gameScripts/playVideo.tsx +++ b/packages/webgal/src/Core/gameScripts/playVideo.tsx @@ -7,6 +7,7 @@ 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 */ @@ -31,7 +32,7 @@ export const playVideo = (sentence: ISentence): IPerform => { performName: 'none', duration: 0, isHoldOn: false, - stopFunction: () => {}, + stopFunction: () => { }, blockingNext: () => blockingNextFlag, blockingAuto: () => true, stopTimeout: undefined, // 暂时不用,后面会交给自动清除 @@ -69,12 +70,9 @@ export const playVideo = (sentence: ISentence): IPerform => { /** * 恢复音量 */ - const bgmElement: any = document.getElementById('currentBgm'); - if (bgmElement) { - bgmElement.volume = bgmVol.toString(); - } + bgmManager.resume({ volume: bgmVol, fade: 1000 }); const vocalElement: any = document.getElementById('currentVocal'); - if (bgmElement) { + if (vocalElement) { vocalElement.volume = vocalVol.toString(); } // eslint-disable-next-line react/no-deprecated @@ -93,12 +91,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: 1000 }); const vocalElement: any = document.getElementById('currentVocal'); - if (bgmElement) { + if (vocalElement) { vocalElement.volume = vocalVol2.toString(); } diff --git a/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx b/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx index c82e80c92..912fdfe77 100644 --- a/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx +++ b/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx @@ -3,6 +3,7 @@ import { RootState, webgalStore } from '@/store/store'; import { setStage } from '@/store/stageReducer'; import { useEffect, useState } from 'react'; import { logger } from '@/Core/util/logger'; +import { bgmManager } from '@/Core/Modules/audio/bgmManager'; export const AudioContainer = () => { const stageStore = useSelector((webgalStore: RootState) => webgalStore.stage); @@ -19,56 +20,15 @@ export const AudioContainer = () => { const uiSeVol = mainVol * 0.01 * (userDataState.optionData.uiSeVolume ?? 50) * 0.01; const isEnterGame = useSelector((state: RootState) => state.GUI.isEnterGame); - // 淡入淡出定时器 - const [fadeTimer, setFadeTimer] = useState(setTimeout(() => {}, 0)); - - /** - * 淡入BGM - * @param bgm 背景音乐 - * @param maxVol 最大音量 - * @param time 淡入时间 - */ - const bgmFadeIn = (bgm: HTMLAudioElement, maxVol: number, time: number) => { - // 设置初始音量 - time >= 0 ? (bgm.volume = 0) : (bgm.volume = maxVol); - // 设置音量递增时间间隔 - const duration = 10; - // 计算每duration的音量增量 - const volumeStep = (maxVol / time) * duration; - // 基于递归调用实现淡入淡出效果 - const fade = () => { - const timer = setTimeout(() => { - if (bgm.volume + volumeStep >= maxVol) { - // 如果音量接近或达到最大值,则设置最终音量(淡入) - bgm.volume = maxVol; - } else if (bgm.volume + volumeStep <= 0) { - // 如果音量接近或达到最小值,则设置最终音量(淡出) - bgm.volume = 0; - // 淡出效果结束后,将 bgm 置空 - webgalStore.dispatch(setStage({ key: 'bgm', value: { src: '', enter: 0, volume: 100 } })); - } else { - // 否则增加音量,并递归调用 - bgm.volume += volumeStep; - fade(); - } - }, duration); - // 将定时器引用存储到 fadeTimer 中 - setFadeTimer(timer); - }; - // 调用淡入淡出函数 - fade(); - }; - useEffect(() => { - // 清除之前的淡入淡出定时器 - clearTimeout(fadeTimer); - // 获取当前背景音乐元素 - const bgmElement = document.getElementById('currentBgm') as HTMLAudioElement; - // 如果当前背景音乐元素存在,则淡入淡出 - if (bgmElement) { - bgmEnter === 0 ? (bgmElement.volume = bgmVol) : bgmFadeIn(bgmElement, bgmVol, bgmEnter); + if (!isEnterGame) return; + + if (isShowTitle) { + bgmManager.play(titleBgm, { volume: bgmVol, fade: bgmEnter }); + } else { + bgmManager.play(stageStore.bgm.src, { volume: bgmVol, fade: bgmEnter }); } - }, [isShowTitle, titleBgm, stageStore.bgm.src, bgmVol, bgmEnter]); + }, [isEnterGame, isShowTitle, titleBgm, stageStore.bgm.src]); useEffect(() => { logger.debug(`设置背景音量:${bgmVol}`); @@ -118,13 +78,6 @@ export const AudioContainer = () => { return (
-
); diff --git a/packages/webgal/src/UI/Extra/ExtraBgm.tsx b/packages/webgal/src/UI/Extra/ExtraBgm.tsx index 7c9bba9b2..0010d4ffd 100644 --- a/packages/webgal/src/UI/Extra/ExtraBgm.tsx +++ b/packages/webgal/src/UI/Extra/ExtraBgm.tsx @@ -1,5 +1,5 @@ import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from '@/store/store'; +import { RootState, webgalStore } from '@/store/store'; import React from 'react'; import styles from '@/UI/Extra/extra.module.scss'; import { useValue } from '@/hooks/useValue'; @@ -7,12 +7,16 @@ import { setStage } from '@/store/stageReducer'; import { GoEnd, GoStart, MusicList, PlayOne, SquareSmall } from '@icon-park/react'; import useSoundEffect from '@/hooks/useSoundEffect'; import { setGuiAsset } from '@/store/GUIReducer'; +import { bgmManager } from '@/Core/Modules/audio/bgmManager'; export function ExtraBgm() { const { playSeClick, playSeEnter } = useSoundEffect(); // 检查当前正在播放的bgm是否在bgm列表内 const currentBgmSrc = useSelector((state: RootState) => state.GUI.titleBgm); const extraState = useSelector((state: RootState) => state.userData.appreciationData); + const userDataState = useSelector((state: RootState) => state.userData); + const mainVol = userDataState.optionData.volumeMain; + const bgmVol = mainVol * 0.01 * userDataState.optionData.bgmVolume * 0.01; const initName = 'Title_BGM'; // 是否展示 bgm 列表 const isShowBgmList = useValue(false); @@ -88,8 +92,7 @@ export function ExtraBgm() {
{ playSeClick(); - const bgmControl: HTMLAudioElement = document.getElementById('currentBgm') as HTMLAudioElement; - bgmControl?.play().then(); + bgmManager.play(currentBgmSrc, { volume: bgmVol, fade: 1000 }); }} onMouseEnter={playSeEnter} className={styles.bgmControlButton} @@ -113,8 +116,7 @@ export function ExtraBgm() {
{ playSeClick(); - const bgmControl: HTMLAudioElement = document.getElementById('currentBgm') as HTMLAudioElement; - bgmControl.pause(); + bgmManager.stop({ fade: 1000 }); }} onMouseEnter={playSeEnter} className={styles.bgmControlButton} diff --git a/packages/webgal/src/UI/Title/Title.tsx b/packages/webgal/src/UI/Title/Title.tsx index 061df48a6..8485a7e1a 100644 --- a/packages/webgal/src/UI/Title/Title.tsx +++ b/packages/webgal/src/UI/Title/Title.tsx @@ -37,7 +37,6 @@ export default function Title() {
{ - playBgm(GUIState.titleBgm); dispatch(setVisibility({ component: 'isEnterGame', visibility: true })); if (fullScreen === fullScreenOption.on) { document.documentElement.requestFullscreen(); @@ -100,9 +99,8 @@ export default function Title() {
{GUIState.enableAppreciationMode && (
{ if (hasAppreciationItems) { playSeClick(); @@ -125,7 +123,7 @@ export default function Title() { leftFunc: () => { window.close(); }, - rightFunc: () => {}, + rightFunc: () => { }, }); }} onMouseEnter={playSeEnter} diff --git a/yarn.lock b/yarn.lock index 062e2199d..e3b6194ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3165,6 +3165,11 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +gsap@^3.14.2: + version "3.14.2" + resolved "https://registry.yarnpkg.com/gsap/-/gsap-3.14.2.tgz#6a9ea31e5046948e0be61eae006ae576ca5937d6" + integrity sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA== + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" From 9c7c0003f362fcf6336a84047068dc09f519b035 Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:30:55 +0800 Subject: [PATCH 2/3] feat: updated the BgmManager to support new playback options --- .../src/Core/Modules/audio/bgmManager.ts | 255 +++++++++++++----- .../src/Core/controller/stage/playBgm.ts | 2 +- .../webgal/src/Core/gameScripts/playVideo.tsx | 6 +- .../Stage/AudioContainer/AudioContainer.tsx | 4 +- packages/webgal/src/UI/Extra/ExtraBgm.tsx | 6 +- packages/webgal/src/store/stageReducer.ts | 2 +- 6 files changed, 194 insertions(+), 81 deletions(-) diff --git a/packages/webgal/src/Core/Modules/audio/bgmManager.ts b/packages/webgal/src/Core/Modules/audio/bgmManager.ts index 26d95719a..4d3a0b4e6 100644 --- a/packages/webgal/src/Core/Modules/audio/bgmManager.ts +++ b/packages/webgal/src/Core/Modules/audio/bgmManager.ts @@ -10,108 +10,217 @@ class BgmManager { return BgmManager.instance; } - private audios: [HTMLAudioElement, HTMLAudioElement]; - private activeIndex = 0; - private progressListeners: Set<(p: { current: number; total: number; ratio: number }) => void> = new Set(); + 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, index) => { - audio.loop = true; + this._audios = [new Audio(), new Audio()]; + this._audios.forEach((audio) => { + audio.loop = this._loop; + audio.preload = 'auto'; audio.crossOrigin = 'anonymous'; - audio.addEventListener('timeupdate', () => this.handleTimeUpdate(index)); + audio.addEventListener('timeupdate', this._onTimeUpdate); }); } - public async play(src: string, options: { volume: number; fade: number }) { - const nextIndex = (this.activeIndex + 1) % 2; - const currentAudio = this.audios[this.activeIndex]; - - if (currentAudio.src === src) { - if (currentAudio.paused) { - await this.resume(options); + public async play(options: { src?: string; loop?: boolean; volume?: number; fade?: number } = {}): Promise { + const fade = options.fade ?? 0; + if (options.volume !== undefined) this._targetVolume = options.volume; + if (options.loop !== undefined) this.loop = options.loop; + + 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 nextAudio = this.audios[nextIndex]; + const oldIndex = this._currentIndex; + const nextIndex = (this._currentIndex + 1) % 2; + const oldAudio = this._audios[oldIndex]; + const nextAudio = this._audios[nextIndex]; - nextAudio.src = src; - nextAudio.volume = 0; + nextAudio.src = options.src; + nextAudio.volume = fade > 0 ? 0 : this._targetVolume; + nextAudio.muted = this._muted; try { - await nextAudio.play(); + nextAudio.load(); + await new Promise((resolve, reject) => { + const onCanPlay = () => { + nextAudio.removeEventListener('error', onError); + resolve(null); + }; + const onError = (e: any) => { + nextAudio.removeEventListener('canplaythrough', onCanPlay); + reject(e); + }; + nextAudio.addEventListener('canplaythrough', onCanPlay, { once: true }); + nextAudio.addEventListener('error', onError, { once: true }); + }); - this.activeIndex = nextIndex; + 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); + } + } - gsap.killTweensOf(currentAudio, 'volume'); - gsap.to(currentAudio, { - volume: 0, - duration: options.fade / 1000, - ease: 'sine.in', - onComplete: () => currentAudio.pause(), - }); + public async pause({ fade = 0 }: { fade?: number }): Promise { + if (fade > 0) { + await this._setVolume({ index: this._currentIndex, volume: 0, fade, pauseOnEnd: true }); + } else { + this._audio.pause(); + } + } - gsap.killTweensOf(nextAudio, 'volume'); - gsap.to(nextAudio, { - volume: options.volume, - duration: options.fade / 1000, - ease: 'sine.out', - }); - } catch (err) { - console.error('BGM Playback failed:', err); + public async stop({ fade = 0 }: { fade?: number }): Promise { + 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 pause(options: { fade: number }) { - const currentAudio = this.audios[this.activeIndex]; - gsap.killTweensOf(currentAudio, 'volume'); - await gsap.to(currentAudio, { - volume: 0, - duration: options.fade / 1000, - ease: 'sine.in', - onComplete: () => currentAudio.pause(), - }); + public async fade({ volume, fade = 0 }: { volume: number; fade?: number }): Promise { + this._targetVolume = volume; + return this._setVolume({ index: this._currentIndex, volume, fade }); + } + + public async resume({ fade = 0 }: { fade?: number }): Promise { + 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 async resume(options: { volume: number; fade: number }) { - const currentAudio = this.audios[this.activeIndex]; - gsap.killTweensOf(currentAudio, 'volume'); - await gsap.to(currentAudio, { - volume: options.volume, - duration: options.fade / 1000, - ease: 'sine.out', + 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 async stop(options: { fade: number }) { - const currentAudio = this.audios[this.activeIndex]; - gsap.killTweensOf(currentAudio, 'volume'); - await gsap.to(currentAudio, { - volume: 0, - duration: options.fade / 1000, - ease: 'sine.in', - onComplete: () => { - currentAudio.pause(); - currentAudio.currentTime = 0; - }, + public get muted() { + return this._muted; + } + public set muted(value: boolean) { + this._muted = value; + this._audios.forEach((a) => { + a.muted = value; }); } - public subscribeProgress(cb: (p: any) => void) { - this.progressListeners.add(cb); - return () => this.progressListeners.delete(cb); + private _setVolume(params: { + index: number; + volume: number; + fade: number; + stopOnEnd?: boolean; + pauseOnEnd?: boolean; + }): Promise { + 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 handleTimeUpdate(index: number) { - if (index !== this.activeIndex) return; - const audio = this.audios[index]; - const data = { - current: audio.currentTime, - total: audio.duration || 0, - ratio: audio.currentTime / (audio.duration || 1), - }; - this.progressListeners.forEach((cb) => cb(data)); + private _onTimeUpdate = () => { + if (!this._audio.src || 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(); } } diff --git a/packages/webgal/src/Core/controller/stage/playBgm.ts b/packages/webgal/src/Core/controller/stage/playBgm.ts index 6f7f82c5c..72e5a2166 100644 --- a/packages/webgal/src/Core/controller/stage/playBgm.ts +++ b/packages/webgal/src/Core/controller/stage/playBgm.ts @@ -18,5 +18,5 @@ export function playBgm(url: string, enter = 0, volume = 100): void { } else { webgalStore.dispatch(setStage({ key: 'bgm', value: { src: url, enter: enter, volume: volume } })); } - bgmManager.play(url, { volume: volume / 100, fade: enter }); + bgmManager.play({ src: url, volume: volume / 100, fade: enter }); } diff --git a/packages/webgal/src/Core/gameScripts/playVideo.tsx b/packages/webgal/src/Core/gameScripts/playVideo.tsx index a8a4430da..1b4d31c20 100644 --- a/packages/webgal/src/Core/gameScripts/playVideo.tsx +++ b/packages/webgal/src/Core/gameScripts/playVideo.tsx @@ -12,10 +12,12 @@ 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; @@ -70,7 +72,7 @@ export const playVideo = (sentence: ISentence): IPerform => { /** * 恢复音量 */ - bgmManager.resume({ volume: bgmVol, fade: 1000 }); + bgmManager.resume({ fade: bgmEnter }); const vocalElement: any = document.getElementById('currentVocal'); if (vocalElement) { vocalElement.volume = vocalVol.toString(); @@ -91,7 +93,7 @@ export const playVideo = (sentence: ISentence): IPerform => { */ const vocalVol2 = 0; const bgmVol2 = 0; - bgmManager.pause({ fade: 1000 }); + bgmManager.pause({ fade: bgmEnter }); const vocalElement: any = document.getElementById('currentVocal'); if (vocalElement) { vocalElement.volume = vocalVol2.toString(); diff --git a/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx b/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx index 912fdfe77..175ced184 100644 --- a/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx +++ b/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx @@ -24,9 +24,9 @@ export const AudioContainer = () => { if (!isEnterGame) return; if (isShowTitle) { - bgmManager.play(titleBgm, { volume: bgmVol, fade: bgmEnter }); + bgmManager.play({ src: titleBgm, volume: bgmVol, fade: bgmEnter }); } else { - bgmManager.play(stageStore.bgm.src, { volume: bgmVol, fade: bgmEnter }); + bgmManager.play({ src: stageStore.bgm.src, volume: bgmVol, fade: bgmEnter }); } }, [isEnterGame, isShowTitle, titleBgm, stageStore.bgm.src]); diff --git a/packages/webgal/src/UI/Extra/ExtraBgm.tsx b/packages/webgal/src/UI/Extra/ExtraBgm.tsx index 0010d4ffd..71f6ef723 100644 --- a/packages/webgal/src/UI/Extra/ExtraBgm.tsx +++ b/packages/webgal/src/UI/Extra/ExtraBgm.tsx @@ -13,10 +13,12 @@ export function ExtraBgm() { const { playSeClick, playSeEnter } = useSoundEffect(); // 检查当前正在播放的bgm是否在bgm列表内 const currentBgmSrc = useSelector((state: RootState) => state.GUI.titleBgm); + const stageStore = useSelector((webgalStore: RootState) => webgalStore.stage); const extraState = useSelector((state: RootState) => state.userData.appreciationData); const userDataState = useSelector((state: RootState) => state.userData); const mainVol = userDataState.optionData.volumeMain; const bgmVol = mainVol * 0.01 * userDataState.optionData.bgmVolume * 0.01; + const bgmEnter = stageStore.bgm.enter; const initName = 'Title_BGM'; // 是否展示 bgm 列表 const isShowBgmList = useValue(false); @@ -92,7 +94,7 @@ export function ExtraBgm() {
{ playSeClick(); - bgmManager.play(currentBgmSrc, { volume: bgmVol, fade: 1000 }); + bgmManager.play({ src: currentBgmSrc, volume: bgmVol, fade: bgmEnter }); }} onMouseEnter={playSeEnter} className={styles.bgmControlButton} @@ -116,7 +118,7 @@ export function ExtraBgm() {
{ playSeClick(); - bgmManager.stop({ fade: 1000 }); + bgmManager.stop({ fade: bgmEnter }); }} onMouseEnter={playSeEnter} className={styles.bgmControlButton} diff --git a/packages/webgal/src/store/stageReducer.ts b/packages/webgal/src/store/stageReducer.ts index 57e1ffe42..dc94e6cf6 100644 --- a/packages/webgal/src/store/stageReducer.ts +++ b/packages/webgal/src/store/stageReducer.ts @@ -46,7 +46,7 @@ export const initState: IStageState = { bgm: { // 背景音乐 src: '', // 背景音乐 文件地址(相对或绝对) - enter: 0, // 背景音乐 淡入或淡出的毫秒数 + enter: 1000, // 背景音乐 淡入或淡出的毫秒数 volume: 100, // 背景音乐 音量调整(0 - 100) }, uiSe: '', // 用户界面音效 文件地址(相对或绝对) From 9d78db1d3453528f4be4a0915685546b8edd5b93 Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:14:09 +0800 Subject: [PATCH 3/3] feat: refactor BgmManager and remove playBgm --- .../src/Core/Modules/audio/bgmManager.ts | 258 ++++++++++-------- .../src/Core/controller/stage/playBgm.ts | 22 -- packages/webgal/src/Core/gameScripts/bgm.ts | 14 +- packages/webgal/src/Core/gameScripts/end.ts | 6 +- .../webgal/src/Core/gameScripts/playVideo.tsx | 7 +- .../Stage/AudioContainer/AudioContainer.tsx | 21 +- packages/webgal/src/UI/Extra/ExtraBgm.tsx | 11 +- .../src/UI/Menu/MenuPanel/MenuPanel.tsx | 3 +- packages/webgal/src/UI/Title/Title.tsx | 4 +- packages/webgal/src/store/stageInterface.ts | 3 +- packages/webgal/src/store/stageReducer.ts | 3 +- 11 files changed, 183 insertions(+), 169 deletions(-) delete mode 100644 packages/webgal/src/Core/controller/stage/playBgm.ts diff --git a/packages/webgal/src/Core/Modules/audio/bgmManager.ts b/packages/webgal/src/Core/Modules/audio/bgmManager.ts index 4d3a0b4e6..24a080cfd 100644 --- a/packages/webgal/src/Core/Modules/audio/bgmManager.ts +++ b/packages/webgal/src/Core/Modules/audio/bgmManager.ts @@ -1,3 +1,5 @@ +import { setStage } from '@/store/stageReducer'; +import { webgalStore } from '@/store/store'; import gsap from 'gsap'; class BgmManager { @@ -10,218 +12,258 @@ class 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 audios: [HTMLAudioElement, HTMLAudioElement]; + private currentIndex = 0; + private src = ''; + private targetVolume = 100; + 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; + this.audios = [new Audio(), new Audio()]; + this.audios.forEach((audio) => { + audio.loop = true; audio.preload = 'auto'; audio.crossOrigin = 'anonymous'; - audio.addEventListener('timeupdate', this._onTimeUpdate); + audio.addEventListener('timeupdate', this.onTimeUpdate); }); } - public async play(options: { src?: string; loop?: boolean; volume?: number; fade?: number } = {}): Promise { - const fade = options.fade ?? 0; - if (options.volume !== undefined) this._targetVolume = options.volume; - if (options.loop !== undefined) this.loop = options.loop; + /** + * 播放bgm + * @param options.src bgm路径 + * @param options.volume 背景音乐 音量调整(0 - 100) + * @param options.enter 淡入时间(单位毫秒) + * @param options.exit 淡出时间(单位毫秒) + * @param options.loop 是否循环播放 + */ + public async play( + options: { src?: string; volume?: number; enter?: number; exit?: number; loop?: boolean } = {}, + ): Promise { + const src = options.src ?? this.src; + const enter = options.enter ?? 0; + const exit = options.exit ?? enter; + const volume = Math.max(0, Math.min(100, Math.trunc(options.volume ?? this.targetVolume))); + const loop = options.loop ?? this.loop; + + this.targetVolume = volume; + this.loop = loop; + + if (src === '') { + await this.stop(exit); + 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 }); + webgalStore.dispatch(setStage({ key: 'bgm', value: { src, volume, enter, exit } })); + + if (src === this.src) { + if (this.audio.paused) { + this.audio.volume = 0; + await this.audio.play(); } + await this.setVolume({ audio: this.audio, volume: this.targetVolume, duration: enter }); return; } - const oldIndex = this._currentIndex; - const nextIndex = (this._currentIndex + 1) % 2; - const oldAudio = this._audios[oldIndex]; - const nextAudio = this._audios[nextIndex]; + 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; + nextAudio.pause(); + nextAudio.src = src; + nextAudio.volume = 0; try { nextAudio.load(); await new Promise((resolve, reject) => { - const onCanPlay = () => { - nextAudio.removeEventListener('error', onError); + const cleanup = () => { + nextAudio.removeEventListener('canplaythrough', onResolve); + nextAudio.removeEventListener('error', onReject); + }; + + const onResolve = () => { + cleanup(); resolve(null); }; - const onError = (e: any) => { - nextAudio.removeEventListener('canplaythrough', onCanPlay); + + const onReject = (e: Event) => { + cleanup(); reject(e); }; - nextAudio.addEventListener('canplaythrough', onCanPlay, { once: true }); - nextAudio.addEventListener('error', onError, { once: true }); + nextAudio.addEventListener('canplaythrough', onResolve, { once: true }); + nextAudio.addEventListener('error', onReject, { once: true }); }); await nextAudio.play(); - this._currentIndex = nextIndex; + this.currentIndex = nextIndex; - if (fade > 0) { + if (enter > 0) { await Promise.all([ - this._setVolume({ index: oldIndex, volume: 0, fade, stopOnEnd: true }), - this._setVolume({ index: nextIndex, volume: this._targetVolume, fade }), + this.setVolume({ audio: oldAudio, volume: 0, duration: exit, stopOnEnd: true }), + this.setVolume({ audio: nextAudio, volume: this.targetVolume, duration: enter }), ]); } else { - this._stopAudio(oldAudio); + this.resetAudio(oldAudio); } + + this.src = src; } catch (e) { console.error('BGM Playback failed:', e); - this._stopAudio(nextAudio); + this.resetAudio(nextAudio); } } - public async pause({ fade = 0 }: { fade?: number }): Promise { - if (fade > 0) { - await this._setVolume({ index: this._currentIndex, volume: 0, fade, pauseOnEnd: true }); + public async pause(value = 0): Promise { + if (value > 0) { + await this.setVolume({ audio: this.audio, volume: 0, duration: value, pauseOnEnd: true }); } else { - this._audio.pause(); + this.audio.pause(); } } - public async stop({ fade = 0 }: { fade?: number }): Promise { - if (fade > 0) { - await this._setVolume({ index: this._currentIndex, volume: 0, fade, stopOnEnd: true }); + public async stop(value = 0): Promise { + this.src = ''; + this.targetVolume = 100; + webgalStore.dispatch(setStage({ key: 'bgm', value: { src: '', volume: 100, enter: 0, exit: 0 } })); + if (value > 0) { + await this.setVolume({ audio: this.audio, volume: 0, duration: value, stopOnEnd: true }); } else { - this._audios.forEach((_, i) => this._stopAudio(this._audios[i])); + this.audios.forEach((_, i) => this.resetAudio(this.audios[i])); } } - public async fade({ volume, fade = 0 }: { volume: number; fade?: number }): Promise { - this._targetVolume = volume; - return this._setVolume({ index: this._currentIndex, volume, fade }); + public async resume(value = 0): Promise { + return this.play({ enter: value }); } - public async resume({ fade = 0 }: { fade?: number }): Promise { - return this.play({ fade }); + public refreshVolume() { + this.volume = this.targetVolume; } public addProgressListener(cb: (p: { currentTime: number; duration: number }) => void): () => void { - this._progressListeners.add(cb); + this.progressListeners.add(cb); return () => { - this._progressListeners.delete(cb); + this.progressListeners.delete(cb); }; } public clearListeners(): void { - this._progressListeners.clear(); + this.progressListeners.clear(); } - private get _audio() { - return this._audios[this._currentIndex]; + private get audio() { + return this.audios[this.currentIndex]; } public get currentTime() { - return this._audio.currentTime; + return this.audio.currentTime; } public set currentTime(value: number) { - this._audio.currentTime = value; + this.audio.currentTime = value; } public get duration() { - return this._audio.duration; + return this.audio.duration; } public get paused() { - return this._audio.paused; + return this.audio.paused; } public get volume() { - return this._targetVolume; + 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)); + const volume = Math.max(0, Math.min(100, Math.trunc(value))); + this.targetVolume = volume; + + const computedVolume = this.getComputedVolume(volume); + + const activeTweens = gsap.getTweensOf(this.audio, true); + if (activeTweens.length > 0) { + activeTweens.forEach((tween) => { + tween.vars.volume = computedVolume; + tween.invalidate(); + }); + } else { + this.audio.volume = computedVolume; + } } public get loop() { - return this._loop; + return this.audio.loop; } public set loop(value: boolean) { - this._loop = value; - this._audios.forEach((a) => { + 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; - }); + public getComputedVolume(value?: number): number { + const { userData, stage } = webgalStore.getState(); + const { optionData } = userData; + + const main = optionData.volumeMain * 0.01; + const group = optionData.bgmVolume * 0.01; + const current = (value ?? stage.bgm.volume) * 0.01; + + return main * group * current; } - private _setVolume(params: { - index: number; + private setVolume(options: { + audio: HTMLAudioElement; volume: number; - fade: number; + duration: number; stopOnEnd?: boolean; pauseOnEnd?: boolean; }): Promise { - const { index, volume, fade, stopOnEnd, pauseOnEnd } = params; + const { audio, volume, duration, stopOnEnd, pauseOnEnd } = options; - const audio = this._audios[index]; - - if (!audio.src || audio.src === window.location.href) { + if (!audio.src || 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', + const computedVolume = this.getComputedVolume(volume); + const vars: gsap.TweenVars = { + volume: computedVolume, + duration: duration / 1000, + ease: computedVolume > audio.volume ? 'sine.out' : 'sine.in', overwrite: 'auto', onComplete: () => { - if (stopOnEnd) this._stopAudio(audio); + if (stopOnEnd) this.resetAudio(audio); else if (pauseOnEnd) audio.pause(); resolve(); }, onInterrupt: () => resolve(), - }); + }; + + if (duration <= 0) { + gsap.set(audio, vars); + } else { + gsap.to(audio, vars); + } }); } - private _onTimeUpdate = () => { - if (!this._audio.src || this._progressListeners.size === 0) return; - const { currentTime, duration } = this._audio; - this._progressListeners.forEach((listener) => listener({ currentTime, duration })); + private onTimeUpdate = () => { + if (this.src === '' || 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'); + private resetAudio(audio: HTMLAudioElement) { + gsap.killTweensOf(audio); + audio.pause(); + audio.volume = 0; + audio.loop = true; + audio.removeAttribute('src'); audio.load(); } } -export const bgmManager = BgmManager.getInstance(); +const bgmManager = BgmManager.getInstance(); + +export default bgmManager; diff --git a/packages/webgal/src/Core/controller/stage/playBgm.ts b/packages/webgal/src/Core/controller/stage/playBgm.ts deleted file mode 100644 index 72e5a2166..000000000 --- a/packages/webgal/src/Core/controller/stage/playBgm.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { webgalStore } from '@/store/store'; -import { setStage } from '@/store/stageReducer'; -import { logger } from '@/Core/util/logger'; -import { bgmManager } from '@/Core/Modules/audio/bgmManager'; - -/** - * 播放bgm - * @param url bgm路径 - * @param enter 淡入时间(单位毫秒) - * @param volume 背景音乐 音量调整(0 - 100) - */ -export function playBgm(url: string, enter = 0, volume = 100): void { - logger.debug('playing bgm' + url); - if (url === '') { - bgmManager.stop({ fade: enter }); - const lastSrc = webgalStore.getState().stage.bgm.src; - webgalStore.dispatch(setStage({ key: 'bgm', value: { src: lastSrc, enter: -enter, volume: volume } })); - } else { - webgalStore.dispatch(setStage({ key: 'bgm', value: { src: url, enter: enter, volume: volume } })); - } - bgmManager.play({ src: url, volume: volume / 100, fade: enter }); -} diff --git a/packages/webgal/src/Core/gameScripts/bgm.ts b/packages/webgal/src/Core/gameScripts/bgm.ts index d39a88ed7..6ebd34682 100644 --- a/packages/webgal/src/Core/gameScripts/bgm.ts +++ b/packages/webgal/src/Core/gameScripts/bgm.ts @@ -1,11 +1,11 @@ import { ISentence } from '@/Core/controller/scene/sceneInterface'; import { IPerform } from '@/Core/Modules/perform/performInterface'; -import { playBgm } from '@/Core/controller/stage/playBgm'; import { getNumberArgByKey, getStringArgByKey } from '@/Core/util/getSentenceArg'; import { webgalStore } from '@/store/store'; import { unlockBgmInUserData } from '@/store/userDataReducer'; import localforage from 'localforage'; import { WebGAL } from '../WebGAL'; +import bgmManager from '../Modules/audio/bgmManager'; /** * 播放一段bgm @@ -15,24 +15,26 @@ export const bgm = (sentence: ISentence): IPerform => { let url: string = sentence.content; // 获取bgm的url const name = getStringArgByKey(sentence, 'unlockname') ?? ''; const series = getStringArgByKey(sentence, 'series') ?? 'default'; - let enter = getNumberArgByKey(sentence, 'enter') ?? 0; // 获取bgm的淡入时间 - enter = Math.max(0, enter); // 限制淡入时间在 0 以上 let volume = getNumberArgByKey(sentence, 'volume') ?? 100; // 获取bgm的音量比 volume = Math.max(0, Math.min(volume, 100)); // 限制音量在 0-100 之间 + let enter = getNumberArgByKey(sentence, 'enter') ?? 0; // 获取bgm的淡入时间 + enter = Math.max(0, enter); // 限制淡入时间在 0 以上 + let exit = getNumberArgByKey(sentence, 'exit') ?? enter; // 获取bgm的淡出时间 + exit = Math.max(0, exit); // 限制淡出时间在 0 以上 if (name !== '') { webgalStore.dispatch(unlockBgmInUserData({ name, url, series })); const userDataState = webgalStore.getState().userData; - localforage.setItem(WebGAL.gameKey, userDataState).then(() => {}); + localforage.setItem(WebGAL.gameKey, userDataState).then(() => { }); } - playBgm(url, enter, volume); + bgmManager.play({ src: url, volume, enter, exit }); return { performName: 'none', duration: 0, isHoldOn: true, - stopFunction: () => {}, + stopFunction: () => { }, blockingNext: () => false, blockingAuto: () => true, stopTimeout: undefined, // 暂时不用,后面会交给自动清除 diff --git a/packages/webgal/src/Core/gameScripts/end.ts b/packages/webgal/src/Core/gameScripts/end.ts index 3902472bf..02fbb628b 100644 --- a/packages/webgal/src/Core/gameScripts/end.ts +++ b/packages/webgal/src/Core/gameScripts/end.ts @@ -6,7 +6,7 @@ import { sceneParser } from '@/Core/parser/sceneParser'; import { resetStage } from '@/Core/controller/stage/resetStage'; import { webgalStore } from '@/store/store'; import { setVisibility } from '@/store/GUIReducer'; -import { playBgm } from '@/Core/controller/stage/playBgm'; +import bgmManager from '@/Core/Modules/audio/bgmManager'; import { WebGAL } from '@/Core/WebGAL'; import { dumpToStorageFast } from '@/Core/controller/storage/storageController'; import { saveActions } from '@/store/savesReducer'; @@ -31,12 +31,12 @@ export const end = (sentence: ISentence): IPerform => { WebGAL.sceneManager.sceneData.currentScene = sceneParser(rawScene, 'start.txt', sceneUrl); }); dispatch(setVisibility({ component: 'showTitle', visibility: true })); - playBgm(webgalStore.getState().GUI.titleBgm); + bgmManager.play({ src: webgalStore.getState().GUI.titleBgm, volume: 100, enter: 2000 }); return { performName: 'none', duration: 0, isHoldOn: false, - stopFunction: () => {}, + stopFunction: () => { }, blockingNext: () => false, blockingAuto: () => true, stopTimeout: undefined, // 暂时不用,后面会交给自动清除 diff --git a/packages/webgal/src/Core/gameScripts/playVideo.tsx b/packages/webgal/src/Core/gameScripts/playVideo.tsx index 1b4d31c20..d3ee0867b 100644 --- a/packages/webgal/src/Core/gameScripts/playVideo.tsx +++ b/packages/webgal/src/Core/gameScripts/playVideo.tsx @@ -7,7 +7,7 @@ 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'; +import bgmManager from '../Modules/audio/bgmManager'; /** * 播放一段视频 * @param sentence */ @@ -18,6 +18,7 @@ export const playVideo = (sentence: ISentence): IPerform => { 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 bgmExit = stageState.bgm.exit; const performInitName: string = getRandomPerformName(); let blockingNextFlag = getBooleanArgByKey(sentence, 'skipOff') ?? false; @@ -72,7 +73,7 @@ export const playVideo = (sentence: ISentence): IPerform => { /** * 恢复音量 */ - bgmManager.resume({ fade: bgmEnter }); + bgmManager.resume(bgmEnter); const vocalElement: any = document.getElementById('currentVocal'); if (vocalElement) { vocalElement.volume = vocalVol.toString(); @@ -93,7 +94,7 @@ export const playVideo = (sentence: ISentence): IPerform => { */ const vocalVol2 = 0; const bgmVol2 = 0; - bgmManager.pause({ fade: bgmEnter }); + bgmManager.pause(bgmExit); const vocalElement: any = document.getElementById('currentVocal'); if (vocalElement) { vocalElement.volume = vocalVol2.toString(); diff --git a/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx b/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx index 175ced184..544457a2d 100644 --- a/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx +++ b/packages/webgal/src/Stage/AudioContainer/AudioContainer.tsx @@ -3,41 +3,34 @@ import { RootState, webgalStore } from '@/store/store'; import { setStage } from '@/store/stageReducer'; import { useEffect, useState } from 'react'; import { logger } from '@/Core/util/logger'; -import { bgmManager } from '@/Core/Modules/audio/bgmManager'; +import bgmManager from '@/Core/Modules/audio/bgmManager'; export const AudioContainer = () => { const stageStore = useSelector((webgalStore: RootState) => webgalStore.stage); - const titleBgm = useSelector((webgalStore: RootState) => webgalStore.GUI.titleBgm); - const isShowTitle = useSelector((webgalStore: RootState) => webgalStore.GUI.showTitle); const userDataState = useSelector((state: RootState) => state.userData); const mainVol = userDataState.optionData.volumeMain; const vocalBaseVol = mainVol * 0.01 * userDataState.optionData.vocalVolume * 0.01; const vocalVol = vocalBaseVol * stageStore.vocalVolume * 0.01; const bgmVol = mainVol * 0.01 * userDataState.optionData.bgmVolume * 0.01 * stageStore.bgm.volume * 0.01; const bgmEnter = stageStore.bgm.enter; + const bgmExit = stageStore.bgm.exit; const uiSoundEffects = stageStore.uiSe; const seVol = mainVol * 0.01 * (userDataState.optionData?.seVolume ?? 100) * 0.01; const uiSeVol = mainVol * 0.01 * (userDataState.optionData.uiSeVolume ?? 50) * 0.01; - const isEnterGame = useSelector((state: RootState) => state.GUI.isEnterGame); - - useEffect(() => { - if (!isEnterGame) return; - - if (isShowTitle) { - bgmManager.play({ src: titleBgm, volume: bgmVol, fade: bgmEnter }); - } else { - bgmManager.play({ src: stageStore.bgm.src, volume: bgmVol, fade: bgmEnter }); - } - }, [isEnterGame, isShowTitle, titleBgm, stageStore.bgm.src]); useEffect(() => { logger.debug(`设置背景音量:${bgmVol}`); + bgmManager.refreshVolume(); }, [bgmVol]); useEffect(() => { logger.debug(`设置背景音量淡入时间: ${bgmEnter}`); }, [bgmEnter]); + useEffect(() => { + logger.debug(`设置背景音量淡出时间: ${bgmExit}`); + }, [bgmExit]); + useEffect(() => { logger.debug(`设置语音音量:${vocalVol}`); const vocalElement: any = document.getElementById('currentVocal'); diff --git a/packages/webgal/src/UI/Extra/ExtraBgm.tsx b/packages/webgal/src/UI/Extra/ExtraBgm.tsx index 71f6ef723..c7cca56c2 100644 --- a/packages/webgal/src/UI/Extra/ExtraBgm.tsx +++ b/packages/webgal/src/UI/Extra/ExtraBgm.tsx @@ -7,18 +7,13 @@ import { setStage } from '@/store/stageReducer'; import { GoEnd, GoStart, MusicList, PlayOne, SquareSmall } from '@icon-park/react'; import useSoundEffect from '@/hooks/useSoundEffect'; import { setGuiAsset } from '@/store/GUIReducer'; -import { bgmManager } from '@/Core/Modules/audio/bgmManager'; +import bgmManager from '@/Core/Modules/audio/bgmManager'; export function ExtraBgm() { const { playSeClick, playSeEnter } = useSoundEffect(); // 检查当前正在播放的bgm是否在bgm列表内 const currentBgmSrc = useSelector((state: RootState) => state.GUI.titleBgm); - const stageStore = useSelector((webgalStore: RootState) => webgalStore.stage); const extraState = useSelector((state: RootState) => state.userData.appreciationData); - const userDataState = useSelector((state: RootState) => state.userData); - const mainVol = userDataState.optionData.volumeMain; - const bgmVol = mainVol * 0.01 * userDataState.optionData.bgmVolume * 0.01; - const bgmEnter = stageStore.bgm.enter; const initName = 'Title_BGM'; // 是否展示 bgm 列表 const isShowBgmList = useValue(false); @@ -94,7 +89,7 @@ export function ExtraBgm() {
{ playSeClick(); - bgmManager.play({ src: currentBgmSrc, volume: bgmVol, fade: bgmEnter }); + bgmManager.play({ src: currentBgmSrc, volume: 100, enter: 500, exit: 500 }); }} onMouseEnter={playSeEnter} className={styles.bgmControlButton} @@ -118,7 +113,7 @@ export function ExtraBgm() {
{ playSeClick(); - bgmManager.stop({ fade: bgmEnter }); + bgmManager.stop(500); }} onMouseEnter={playSeEnter} className={styles.bgmControlButton} diff --git a/packages/webgal/src/UI/Menu/MenuPanel/MenuPanel.tsx b/packages/webgal/src/UI/Menu/MenuPanel/MenuPanel.tsx index 85e1100d6..ac76a275e 100644 --- a/packages/webgal/src/UI/Menu/MenuPanel/MenuPanel.tsx +++ b/packages/webgal/src/UI/Menu/MenuPanel/MenuPanel.tsx @@ -1,6 +1,5 @@ import styles from './menuPanel.module.scss'; import { MenuPanelButton } from './MenuPanelButton'; -import { playBgm } from '@/Core/controller/stage/playBgm'; import { MenuPanelTag } from '@/store/guiInterface'; import { useDispatch, useSelector } from 'react-redux'; import { RootState } from '@/store/store'; @@ -80,7 +79,7 @@ export const MenuPanel = () => { backToTitle(); dispatch(setVisibility({ component: 'showMenuPanel', visibility: false })); }, - rightFunc: () => {}, + rightFunc: () => { }, }); }} tagName={t('title.title')} diff --git a/packages/webgal/src/UI/Title/Title.tsx b/packages/webgal/src/UI/Title/Title.tsx index 8485a7e1a..e2a7dd101 100644 --- a/packages/webgal/src/UI/Title/Title.tsx +++ b/packages/webgal/src/UI/Title/Title.tsx @@ -8,13 +8,14 @@ import useSoundEffect from '@/hooks/useSoundEffect'; import useApplyStyle from '@/hooks/useApplyStyle'; import { keyboard } from '@/hooks/useHotkey'; import useConfigData from '@/hooks/useConfigData'; -import { playBgm } from '@/Core/controller/stage/playBgm'; import { continueGame, startGame } from '@/Core/controller/gamePlay/startContinueGame'; import { showGlogalDialog } from '../GlobalDialog/GlobalDialog'; import styles from './title.module.scss'; +import bgmManager from '@/Core/Modules/audio/bgmManager'; /** 标题页 */ export default function Title() { + const stageStore = useSelector((webgalStore: RootState) => webgalStore.stage); const userDataState = useSelector((state: RootState) => state.userData); const GUIState = useSelector((state: RootState) => state.GUI); const dispatch = useDispatch(); @@ -37,6 +38,7 @@ export default function Title() {
{ + bgmManager.play({ src: GUIState.titleBgm, volume: 100, enter: 2000 }); dispatch(setVisibility({ component: 'isEnterGame', visibility: true })); if (fullScreen === fullScreenOption.on) { document.documentElement.requestFullscreen(); diff --git a/packages/webgal/src/store/stageInterface.ts b/packages/webgal/src/store/stageInterface.ts index 907df19be..be50cc77f 100644 --- a/packages/webgal/src/store/stageInterface.ts +++ b/packages/webgal/src/store/stageInterface.ts @@ -213,8 +213,9 @@ export interface IStageState { bgm: { // 背景音乐 src: string; // 背景音乐 文件地址(相对或绝对) - enter: number; // 背景音乐 淡入或淡出的毫秒数 volume: number; // 背景音乐 音量调整(0 - 100) + enter: number; // 背景音乐 淡入的毫秒数 + exit: number; // 背景音乐 淡出的毫秒数 }; uiSe: string; // 用户界面音效 文件地址(相对或绝对) miniAvatar: string; // 小头像 文件地址(相对或绝对) diff --git a/packages/webgal/src/store/stageReducer.ts b/packages/webgal/src/store/stageReducer.ts index dc94e6cf6..90dee34ff 100644 --- a/packages/webgal/src/store/stageReducer.ts +++ b/packages/webgal/src/store/stageReducer.ts @@ -46,8 +46,9 @@ export const initState: IStageState = { bgm: { // 背景音乐 src: '', // 背景音乐 文件地址(相对或绝对) - enter: 1000, // 背景音乐 淡入或淡出的毫秒数 volume: 100, // 背景音乐 音量调整(0 - 100) + enter: 0, // 背景音乐 淡入的毫秒数 + exit: 0, // 背景音乐 淡出的毫秒数 }, uiSe: '', // 用户界面音效 文件地址(相对或绝对) miniAvatar: '', // 小头像 文件地址(相对或绝对)