diff --git a/public/libs/control.js b/public/libs/control.js index 7fca7d4..e1736af 100644 --- a/public/libs/control.js +++ b/public/libs/control.js @@ -526,17 +526,20 @@ control.prototype.setHeroMoveInterval = function (callback) { // render.move(true); // }); - core.interval.heroMoveInterval = window.setInterval(function () { - // render.offset += toAdd * 4; - core.status.heroMoving += toAdd; - if (core.status.heroMoving >= 8) { - clearInterval(core.interval.heroMoveInterval); - core.status.heroMoving = 0; - // render.offset = 0; - // render.move(false); - if (callback) callback(); - } - }, ((core.values.moveSpeed / 8) * toAdd) / core.status.replay.speed); + core.interval.heroMoveInterval = window.setInterval( + function () { + // render.offset += toAdd * 4; + core.status.heroMoving += toAdd; + if (core.status.heroMoving >= 8) { + clearInterval(core.interval.heroMoveInterval); + core.status.heroMoving = 0; + // render.offset = 0; + // render.move(false); + if (callback) callback(); + } + }, + ((core.values.moveSpeed / 8) * toAdd) / core.status.replay.speed + ); }; ////// 每移动一格后执行的事件 ////// @@ -2988,25 +2991,23 @@ control.prototype.screenFlash = function ( // todo: deprecate playBgm, pauseBgm, resumeBgm, triggerBgm ////// 播放背景音乐 ////// control.prototype.playBgm = function (bgm, startTime) { - bgm = core.getMappedName(bgm); - if (main.mode !== 'play') return; - Mota.require('var', 'bgm').changeTo(bgm, startTime); + // see src/module/fallback/audio.ts }; ////// 暂停背景音乐的播放 ////// control.prototype.pauseBgm = function () { - if (main.mode !== 'play') return; - Mota.require('var', 'bgm').pause(); + // see src/module/fallback/audio.ts }; ////// 恢复背景音乐的播放 ////// control.prototype.resumeBgm = function (resumeTime) { - if (main.mode !== 'play') return; - Mota.require('var', 'bgm').resume(); + // see src/module/fallback/audio.ts }; ////// 更改背景音乐的播放 ////// control.prototype.triggerBgm = function () { + // see src/module/fallback/audio.ts + return; if (main.mode !== 'play') return; const bgm = Mota.require('var', 'bgm'); bgm.disable = !bgm.disable; @@ -3036,6 +3037,8 @@ control.prototype.getPlayingSounds = function (name) { ////// 检查bgm状态 ////// control.prototype.checkBgm = function () { + // see src/module/fallback/audio.ts + return; const bgm = Mota.require('var', 'bgm'); if (bgm.disable) { bgm.pause(); diff --git a/public/libs/events.js b/public/libs/events.js index 590f098..b88a27f 100644 --- a/public/libs/events.js +++ b/public/libs/events.js @@ -696,12 +696,12 @@ events.prototype.getItem = function (id, num, x, y, isGentleClick, callback) { (id.endsWith('Key') ? '(钥匙类道具,遇到对应的门时自动打开)' : itemCls == 'tools' - ? '(消耗类道具,请按T在道具栏使用)' - : itemCls == 'constants' - ? '(永久类道具,请按T在道具栏使用)' - : itemCls == 'equips' - ? '(装备类道具,请按Q在装备栏进行装备)' - : '') + ? '(消耗类道具,请按T在道具栏使用)' + : itemCls == 'constants' + ? '(永久类道具,请按T在道具栏使用)' + : itemCls == 'equips' + ? '(装备类道具,请按Q在装备栏进行装备)' + : '') ); } itemHint.push(id); diff --git a/src/common/patch.ts b/src/common/patch.ts new file mode 100644 index 0000000..872670b --- /dev/null +++ b/src/common/patch.ts @@ -0,0 +1,128 @@ +import { logger } from '@/core/common/logger'; + +export const enum PatchClass { + Actions, + Control, + Core, + Data, + Enemys, + Events, + Icons, + Items, + Loader, + Maps, + UI, + Utils +} + +interface PatchList { + [PatchClass.Actions]: Actions; + [PatchClass.Control]: Control; + [PatchClass.Core]: Core; + [PatchClass.Data]: Omit; + [PatchClass.Enemys]: Enemys; + [PatchClass.Events]: Events; + [PatchClass.Icons]: Icons; + [PatchClass.Items]: Items; + [PatchClass.Loader]: Loader; + [PatchClass.Maps]: Maps; + [PatchClass.UI]: Ui; + [PatchClass.Utils]: Utils; +} + +const patchName = { + [PatchClass.Actions]: 'actions', + [PatchClass.Control]: 'control', + [PatchClass.Core]: 'core', + [PatchClass.Data]: 'data', + [PatchClass.Enemys]: 'enemys', + [PatchClass.Events]: 'events', + [PatchClass.Icons]: 'icons', + [PatchClass.Items]: 'items', + [PatchClass.Loader]: 'loader', + [PatchClass.Maps]: 'maps', + [PatchClass.UI]: 'ui', + [PatchClass.Utils]: 'utils' +}; + +export class Patch { + private static patchList: Set> = new Set(); + private static patched: Partial>> = {}; + + private patches: Map any> = new Map(); + + constructor(public readonly patchClass: T) { + Patch.patchList.add(this); + } + + /** + * 添加函数修改 + * @param key 要修改的函数名 + * @param patch 修改为的函数内容 + */ + add< + K extends Exclude< + SelectKey any>, + symbol | number + > + >(key: K, patch: PatchList[T][K]) { + if (this.patches.has(key)) { + logger.warn(49, patchName[this.patchClass], key); + } + this.patches.set(key, patch); + } + + private static getPatchClass(patch: PatchClass): any { + switch (patch) { + case PatchClass.Actions: + return actions.prototype; + case PatchClass.Control: + return control.prototype; + case PatchClass.Core: + return core; + case PatchClass.Data: + return data.prototype; + case PatchClass.Enemys: + return enemys.prototype; + case PatchClass.Events: + return events.prototype; + case PatchClass.Icons: + return icons.prototype; + case PatchClass.Items: + return items.prototype; + case PatchClass.Loader: + return loader.prototype; + case PatchClass.Maps: + return maps.prototype; + case PatchClass.UI: + return ui.prototype; + case PatchClass.Utils: + return utils.prototype; + } + } + + /** + * 修改添加的所有函数 + */ + static patchAll() { + this.patchList.forEach(v => this.patch(v)); + } + + /** + * 修改某个实例添加的所有函数 + * @param patch 要修改的函数实例 + */ + static patch(patch: Patch) { + const patchClass = patch.patchClass; + this.patched[patchClass] ??= new Set(); + const set = this.patched[patchClass]; + const obj = this.getPatchClass(patchClass); + for (const [key, func] of patch.patches) { + if (set.has(key)) { + logger.warn(49, patchName[patchClass], key); + } + obj[key] = func; + } + this.patchList.delete(patch); + } +} diff --git a/src/core/audio/bgm.ts b/src/core/audio/bgm.ts deleted file mode 100644 index c969c0a..0000000 --- a/src/core/audio/bgm.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { Animation, TimingFn, Transition, linear, sleep } from 'mutate-animate'; -import { Undoable } from '../interface'; -import { ResourceController } from '../loader/controller'; -import { has } from '@/plugin/utils'; - -interface AnimatingBgm { - end: () => void; - ani: Transition; - timeout: number; - currentTime: number; - endVolume: number; -} - -export class BgmController - extends ResourceController - implements Undoable -{ - /** Bgm播放栈,可以undo,最多存放10个 */ - stack: BgmIds[] = []; - /** Bgm的redo栈,最多存放10个 */ - redoStack: BgmIds[] = []; - /** 当前播放的bgm */ - now?: BgmIds; - - /** 渐变切歌时长 */ - transitionTime: number = 2000; - /** 渐变切歌的音量曲线 */ - transitionCurve: TimingFn = linear(); - - /** 音量 */ - volume: number = 1; - /** 是否关闭了bgm */ - disable: boolean = false; - - /** 是否正在播放bgm */ - playing: boolean = false; - - private transitionData: Map = new Map(); - - private canChange: boolean = true; - - /** - * 屏蔽音乐修改 - */ - blockChange() { - this.canChange = false; - } - - /** - * 取消屏蔽音乐修改 - */ - unblockChange() { - this.canChange = true; - } - - /** - * 添加一个bgm - * @param uri bgm的`uri`,由于bgm是一类资源,因此`uri`为`bgms.xxx`的形式 - * @param data bgm音频元素 - */ - add(uri: string, data: HTMLAudioElement) { - if (this.list[uri]) { - console.warn(`Repeated bgm: '${uri}'.`); - } - this.list[uri] = data; - data.loop = true; - } - - /** - * 加载一个bgm - * @param id 要加载的bgm - */ - load(id: BgmIds) { - const bgm = this.get(id); - bgm.load(); - } - - /** - * 切换bgm,具有渐变效果,可以通过监听切换事件,同时调用preventDefault来阻止渐变, - * 并使用自己的切歌程序。阻止后,不会将切换的歌曲加入播放栈,也不会进行切歌, - * 所有的切歌操作均由你自己的程序执行 - * @param id 要切换至的bgm - * @param when 切换至的歌从什么时候开始播放,默认-1,表示不改变,整数表示设置为目标值 - */ - changeTo(id: BgmIds, when: number = -1, noStack: boolean = false) { - if (!this.canChange) return; - if (id === this.now) return this.resume(); - let prevent = false; - const preventDefault = () => { - prevent = true; - }; - const ev = { preventDefault }; - - this.emit('changeBgm', ev, id, this.now); - - if (prevent) return; - - this.playing = true; - if (!this.disable) { - this.setTransitionAnimate(id, 1, when); - if (this.now) this.setTransitionAnimate(this.now, 0); - } else { - this.playing = false; - } - - if (!noStack) { - if (this.now) this.stack.push(this.now); - this.redoStack = []; - } - this.now = id; - } - - /** - * 暂停当前bgm的播放,继续播放时将会延续暂停的时刻,同样可以使用preventDefault使用自己的暂停程序 - * @param transition 是否使用渐变效果,默认使用 - */ - pause(transition: boolean = true) { - if (!this.canChange) return; - if (!this.now) return; - let prevent = false; - const preventDefault = () => { - prevent = true; - }; - const ev = { preventDefault }; - - this.emit('pause', ev, this.now); - - if (prevent) return; - - this.playing = false; - - if (transition) this.setTransitionAnimate(this.now, 0); - else this.get(this.now).pause(); - } - - /** - * 继续当前bgm的播放,从上一次暂停的时刻开始播放,同样可以使用preventDefault使用自己的播放程序 - * @param transition 是否使用渐变效果,默认使用 - */ - resume(transition: boolean = true) { - if (!this.canChange) return; - if (!this.now) return; - let prevent = false; - const preventDefault = () => { - prevent = true; - }; - const ev = { preventDefault }; - - this.emit('resume', ev, this.now); - - if (prevent) return; - - this.playing = true; - - if (!this.disable) { - if (transition) this.setTransitionAnimate(this.now, 1); - else this.get(this.now).play(); - } else { - this.playing = false; - } - } - - /** - * 播放bgm,不进行渐变操作,效果为没有渐变的切歌,也会触发changeBgm事件,可以被preventDefault - * @param id 要播放的bgm - * @param when 从bgm的何时开始播放 - */ - play(id: BgmIds, when: number = 0, noStack: boolean = false) { - if (!this.canChange) return; - if (id === this.now) return; - let prevent = false; - const preventDefault = () => { - prevent = true; - }; - const ev = { preventDefault }; - - this.emit('changeBgm', ev, id, this.now); - - if (prevent) return; - - this.playing = true; - - const before = this.now ? null : this.get(this.now!); - const to = this.get(id); - if (before) { - before.pause(); - } - to.currentTime = when; - to.volume = this.volume; - to.play(); - - if (!this.disable) { - if (!noStack) { - if (this.now) this.stack.push(this.now); - this.redoStack = []; - } - this.now = id; - } else { - this.playing = false; - } - } - - /** - * 撤销当前播放,改为播放前一个bgm - */ - undo(transition: boolean = true, when: number = 0) { - if (!this.canChange) return; - if (this.stack.length === 0) return; - else { - const to = this.stack.pop()!; - if (this.now) this.redoStack.push(this.now); - else return; - - if (transition) this.changeTo(to, when, true); - else this.play(to, when, true); - return this.now; - } - } - - /** - * 取消上一次的撤销,改为播放上一次撤销的bgm - */ - redo(transition: boolean = true, when: number = 0) { - if (!this.canChange) return; - if (this.redoStack.length === 0) return; - else { - const to = this.redoStack.pop()!; - if (this.now) this.stack.push(this.now); - else return; - - if (transition) this.changeTo(to, when, true); - else this.play(to, when, true); - return this.now; - } - } - - /** - * 设置渐变切歌信息 - * @param time 渐变时长 - * @param curve 渐变的音量曲线 - */ - setTransition(time?: number, curve?: TimingFn) { - has(time) && (this.transitionTime = time); - has(curve) && (this.transitionCurve = curve); - } - - /** - * 根据id获取bgm - * @param id 要获取的bgm的id - */ - get(id: BgmIds) { - return this.list[`bgms.${id}`]; - } - - private setTransitionAnimate(id: BgmIds, to: number, when: number = -1) { - const bgm = this.get(id); - - let tran = this.transitionData.get(id); - if (!tran) { - const ani = new Transition(); - ani.value.volume = bgm.paused ? 0 : 1; - const end = () => { - ani.ticker.destroy(); - if (tran!.endVolume === 0) { - bgm.pause(); - } else { - bgm.volume = tran!.endVolume * this.volume; - } - this.transitionData.delete(id); - }; - tran = { - end, - ani: ani, - timeout: -1, - currentTime: bgm.currentTime, - endVolume: to - }; - this.transitionData.set(id, tran); - ani.ticker.add(() => { - bgm.volume = ani.value.volume * this.volume; - }); - } - - if (to !== 0) { - bgm.volume = tran.ani.value.volume * this.volume; - if (bgm.paused) bgm.play(); - } - if (when !== -1) { - bgm.currentTime = when; - } - tran.endVolume = to; - - tran.ani - .time(this.transitionTime) - .mode(this.transitionCurve) - .absolute() - .transition('volume', to); - - if (tran.timeout !== -1) { - clearTimeout(tran.timeout); - } - tran.timeout = window.setTimeout(tran.end, this.transitionTime); - } -} - -export const bgm = new BgmController(); diff --git a/src/core/common/resource.ts b/src/core/common/resource.ts index 2625ea2..c90903f 100644 --- a/src/core/common/resource.ts +++ b/src/core/common/resource.ts @@ -475,14 +475,14 @@ export function loadDefaultResource() { const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d; const icon = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1; // bgm - data.main.bgms.forEach(v => { - const res = LoadTask.add('audio', `audio/${v}`); - Mota.r(() => { - res.once('loadStart', res => { - Mota.require('var', 'bgm').add(`bgms.${v}`, res.resource!); - }); - }); - }); + // data.main.bgms.forEach(v => { + // const res = LoadTask.add('audio', `audio/${v}`); + // Mota.r(() => { + // res.once('loadStart', res => { + // Mota.require('var', 'bgm').add(`bgms.${v}`, res.resource!); + // }); + // }); + // }); // fonts data.main.fonts.forEach(v => { const res = LoadTask.add('buffer', `buffer/project/fonts/${v}.ttf`); @@ -584,16 +584,16 @@ export async function loadCompressedResource() { }); const list: CompressedLoadList = JSON.parse(data.data); - const d = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d; + // const d = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d; // 对于bgm,直接按照原来的方式加载即可 - d.main.bgms.forEach(v => { - const res = LoadTask.add('audio', `audio/${v}`); - Mota.r(() => { - res.once('loadStart', res => { - Mota.require('var', 'bgm').add(`bgms.${v}`, res.resource!); - }); - }); - }); + // d.main.bgms.forEach(v => { + // const res = LoadTask.add('audio', `audio/${v}`); + // Mota.r(() => { + // res.once('loadStart', res => { + // Mota.require('var', 'bgm').add(`bgms.${v}`, res.resource!); + // }); + // }); + // }); // 对于区域内容,按照zip格式进行加载,然后解压处理 const autotiles: Partial, HTMLImageElement>> = {}; diff --git a/src/core/index.ts b/src/core/index.ts index 4ecea01..ea2bb90 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,4 +1,3 @@ -import { BgmController, bgm } from './audio/bgm'; import { SoundController, SoundEffect, sound } from './audio/sound'; import { Focus, GameUi, UiController } from './main/custom/ui'; import { GameStorage } from './main/storage'; @@ -11,7 +10,7 @@ import { mainSetting, settingStorage } from './main/setting'; -import { KeyCode, ScanCode } from '@/plugin/keyCodes'; +import { KeyCode } from '@/plugin/keyCodes'; import { status } from '@/plugin/ui/statusBar'; import '@/plugin'; import './package'; @@ -80,7 +79,6 @@ import { TextboxStore } from './render/index'; // ----- 类注册 Mota.register('class', 'AudioPlayer', AudioPlayer); -Mota.register('class', 'BgmController', BgmController); Mota.register('class', 'CustomToolbar', CustomToolbar); Mota.register('class', 'Focus', Focus); Mota.register('class', 'GameStorage', GameStorage); @@ -106,7 +104,6 @@ Mota.register('fn', 'removeAnimate', removeAnimate); // ----- 变量注册 Mota.register('var', 'mainUi', mainUi); Mota.register('var', 'fixedUi', fixedUi); -Mota.register('var', 'bgm', bgm); Mota.register('var', 'sound', sound); Mota.register('var', 'gameKey', gameKey); Mota.register('var', 'mainSetting', mainSetting); diff --git a/src/core/main/setting.ts b/src/core/main/setting.ts index d78b6ca..c635d35 100644 --- a/src/core/main/setting.ts +++ b/src/core/main/setting.ts @@ -3,13 +3,13 @@ import { EventEmitter } from '../common/eventEmitter'; import { GameStorage } from './storage'; import { has, triggerFullscreen } from '@/plugin/utils'; import { createSettingComponents } from './init/settings'; -import { bgm } from '../audio/bgm'; import { SoundEffect } from '../audio/sound'; import settingsText from '@/data/settings.json'; import { isMobile } from '@/plugin/use'; import { fontSize } from '@/plugin/ui/statusBar'; import { CustomToolbar } from './custom/toolbar'; import { fixedUi } from './init/ui'; +import { bgmController } from '@/module'; export interface SettingComponentProps { item: MotaSettingItem; @@ -347,7 +347,7 @@ const root = document.getElementById('root') as HTMLDivElement; function handleScreenSetting( key: string, n: T, - o: T + _o: T ) { if (key === 'fullscreen') { // 全屏 @@ -369,7 +369,7 @@ function handleScreenSetting( function handleActionSetting( key: string, n: T, - o: T + _o: T ) { if (key === 'autoSkill') { // 自动切换技能 @@ -382,13 +382,13 @@ function handleActionSetting( function handleAudioSetting( key: string, n: T, - o: T + _o: T ) { - if (key === 'bgmEnabled') { - bgm.disable = !n; - if (core.isPlaying()) core.checkBgm(); + if (key === 'bgmEnabled') { + bgmController.setEnabled(n as boolean); + core.checkBgm(); } else if (key === 'bgmVolume') { - bgm.volume = (n as number) / 100; + bgmController.setVolume((n as number) / 100); } else if (key === 'soundEnabled') { SoundEffect.disable = !n; } else if (key === 'soundVolume') { diff --git a/src/data/logger.json b/src/data/logger.json index 2811867..0e8865e 100644 --- a/src/data/logger.json +++ b/src/data/logger.json @@ -80,6 +80,8 @@ "46": "Cannot pipe new StreamReader object when stream is loading.", "47": "Audio stream decoder for audio type '$1' has already existed.", "48": "Sample rate in stream audio must be constant.", + "49": "Repeated patch for '$1', key: '$2'.", + "50": "Unknown audio extension name: '$1'", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.", "1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance." } diff --git a/src/game/system.ts b/src/game/system.ts index 3b5f352..d060fa5 100644 --- a/src/game/system.ts +++ b/src/game/system.ts @@ -1,5 +1,4 @@ import type { AudioPlayer } from '@/core/audio/audio'; -import type { BgmController } from '@/core/audio/bgm'; import type { SoundController, SoundEffect } from '@/core/audio/sound'; import type { Disposable } from '@/core/common/disposable'; import type { @@ -61,7 +60,6 @@ interface ClassInterface { AudioPlayer: typeof AudioPlayer; SoundEffect: typeof SoundEffect; SoundController: typeof SoundController; - BgmController: typeof BgmController; Danmaku: typeof Danmaku; // todo: 放到插件 ShaderEffect: typeof ShaderEffect; // 定义于游戏进程,渲染进程依然可用 @@ -92,7 +90,6 @@ interface VariableInterface { fixedUi: UiController; KeyCode: typeof KeyCode; // isMobile: boolean; - bgm: BgmController; sound: SoundController; settingStorage: GameStorage; status: Ref; @@ -431,7 +428,7 @@ class Mota { static require(type: InterfaceType, key: string): any { const data = this.getByType(type)[key]; - if (!!data) return data; + if (data) return data; else { throw new Error( `Cannot resolve require: type='${type}',key='${key}'` @@ -457,10 +454,10 @@ class Mota { return type === 'class' ? this.classes : type === 'fn' - ? this.functions - : type === 'var' - ? this.variables - : this.modules; + ? this.functions + : type === 'var' + ? this.variables + : this.modules; } } diff --git a/src/module/audio/bgm.ts b/src/module/audio/bgm.ts new file mode 100644 index 0000000..e2b3766 --- /dev/null +++ b/src/module/audio/bgm.ts @@ -0,0 +1,252 @@ +import EventEmitter from 'eventemitter3'; +import { audioPlayer, AudioPlayer, AudioRoute, AudioStatus } from './player'; +import { guessTypeByExt, isAudioSupport } from './support'; +import { logger } from '@/core/common/logger'; +import { StreamLoader } from '../loader'; +import { linear, sleep, Transition } from 'mutate-animate'; +import { VolumeEffect } from './effect'; + +interface BgmVolume { + effect: VolumeEffect; + transition: Transition; +} + +interface BgmControllerEvent {} + +export class BgmController< + T extends string = BgmIds +> extends EventEmitter { + /** bgm音频名称的前缀 */ + prefix: string = 'bgms.'; + /** 每个 bgm 的音量控制器 */ + readonly gain: Map = new Map(); + + /** 正在播放的 bgm */ + playingBgm?: T; + /** 是否正在播放 */ + playing: boolean = false; + + /** 是否已经启用 */ + enabled: boolean = true; + /** 主音量控制器 */ + private readonly mainGain: VolumeEffect; + /** 是否屏蔽所有的音乐切换 */ + private blocking: boolean = false; + /** 渐变时长 */ + private transitionTime: number = 2000; + + constructor(public readonly player: AudioPlayer) { + super(); + this.mainGain = player.createVolumeEffect(); + } + + /** + * 设置音频渐变时长 + * @param time 渐变时长 + */ + setTransitionTime(time: number) { + this.transitionTime = time; + for (const [, value] of this.gain) { + value.transition.time(time); + } + } + + /** + * 屏蔽音乐切换 + */ + blockChange() { + this.blocking = true; + } + + /** + * 取消屏蔽音乐切换 + */ + unblockChange() { + this.blocking = false; + } + + /** + * 设置总音量大小 + * @param volume 音量大小 + */ + setVolume(volume: number) { + this.mainGain.setVolume(volume); + } + + /** + * 设置是否启用 + * @param enabled 是否启用 + */ + setEnabled(enabled: boolean) { + if (enabled) this.resume(); + else this.stop(); + this.enabled = enabled; + } + + /** + * 设置 bgm 音频名称的前缀 + */ + setPrefix(prefix: string) { + this.prefix = prefix; + } + + private getId(name: T) { + return `${this.prefix}${name}`; + } + + /** + * 根据 bgm 名称获取其 AudioRoute 实例 + * @param id 音频名称 + */ + get(id: T) { + return this.player.getRoute(this.getId(id)); + } + + /** + * 添加一个 bgm + * @param id 要添加的 bgm 的名称 + * @param url 指定 bgm 的加载地址 + */ + addBgm(id: T, url: string = `project/bgms/${id}`) { + const type = guessTypeByExt(id); + if (!type) { + logger.warn(50, id.split('.').slice(0, -1).join('.')); + return; + } + const gain = this.player.createVolumeEffect(); + if (isAudioSupport(type)) { + const source = audioPlayer.createElementSource(); + source.setSource(url); + source.setLoop(true); + const route = new AudioRoute(source, audioPlayer); + route.addEffect([gain, this.mainGain]); + audioPlayer.addRoute(this.getId(id), route); + this.setTransition(id, route, gain); + } else { + const source = audioPlayer.createStreamSource(); + const stream = new StreamLoader(url); + stream.pipe(source); + source.setLoop(true); + const route = new AudioRoute(source, audioPlayer); + route.addEffect([gain, this.mainGain]); + audioPlayer.addRoute(this.getId(id), route); + this.setTransition(id, route, gain); + } + } + + /** + * 移除一个 bgm + * @param id 要移除的 bgm 的名称 + */ + removeBgm(id: T) { + this.player.removeRoute(this.getId(id)); + const gain = this.gain.get(id); + gain?.transition.ticker.destroy(); + this.gain.delete(id); + } + + private setTransition(id: T, route: AudioRoute, gain: VolumeEffect) { + const transition = new Transition(); + transition + .time(this.transitionTime) + .mode(linear()) + .transition('volume', 0); + + const tick = () => { + gain.setVolume(transition.value.volume); + }; + + /** + * @param expect 在结束时应该是正在播放还是停止 + */ + const setTick = async (expect: AudioStatus) => { + transition.ticker.remove(tick); + transition.ticker.add(tick); + const identifier = route.stopIdentifier; + await sleep(this.transitionTime + 500); + if ( + route.status === expect && + identifier === route.stopIdentifier + ) { + transition.ticker.remove(tick); + if (route.status === AudioStatus.Playing) { + gain.setVolume(1); + } else { + gain.setVolume(0); + } + } + }; + + route.onStart(async () => { + transition.transition('volume', 1); + setTick(AudioStatus.Playing); + }); + route.onEnd(() => { + transition.transition('volume', 0); + setTick(AudioStatus.Paused); + }); + route.setEndTime(this.transitionTime); + + this.gain.set(id, { effect: gain, transition }); + } + + /** + * 播放一个 bgm + * @param id 要播放的 bgm 名称 + */ + play(id: T, when?: number) { + if (this.blocking) return; + if (id !== this.playingBgm && this.playingBgm) { + this.player.pause(this.getId(this.playingBgm)); + } + this.playingBgm = id; + if (!this.enabled) return; + this.player.play(this.getId(id), when); + this.playing = true; + } + + /** + * 继续当前的 bgm + */ + resume() { + if (this.blocking || !this.enabled || this.playing) return; + if (this.playingBgm) { + this.player.resume(this.getId(this.playingBgm)); + } + this.playing = true; + } + + /** + * 暂停当前的 bgm + */ + pause() { + if (this.blocking || !this.enabled) return; + if (this.playingBgm) { + this.player.pause(this.getId(this.playingBgm)); + } + this.playing = false; + } + + /** + * 停止当前的 bgm + */ + stop() { + if (this.blocking || !this.enabled) return; + if (this.playingBgm) { + this.player.stop(this.getId(this.playingBgm)); + } + this.playing = false; + } +} + +export const bgmController = new BgmController(audioPlayer); + +export function loadAllBgm() { + const loading = Mota.require('var', 'loading'); + loading.once('coreInit', () => { + const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d; + for (const bgm of data.main.bgms) { + bgmController.addBgm(bgm); + } + }); +} diff --git a/src/module/audio/bgmLoader.ts b/src/module/audio/bgmLoader.ts deleted file mode 100644 index e841b39..0000000 --- a/src/module/audio/bgmLoader.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { StreamLoader } from '../loader'; -import { audioPlayer, AudioRoute } from './player'; -import { guessTypeByExt, isAudioSupport } from './support'; - -export function loadAllBgm() { - const loading = Mota.require('var', 'loading'); - loading.once('coreInit', () => { - const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d; - for (const bgm of data.main.bgms) { - const type = guessTypeByExt(bgm); - if (!type) continue; - if (isAudioSupport(type)) { - const source = audioPlayer.createElementSource(); - source.setSource(`project/bgms/${bgm}`); - source.setLoop(true); - const route = new AudioRoute(source, audioPlayer); - audioPlayer.addRoute(`bgms.${bgm}`, route); - } else { - const source = audioPlayer.createStreamSource(); - const stream = new StreamLoader(`project/bgms/${bgm}`); - stream.pipe(source); - source.setLoop(true); - const route = new AudioRoute(source, audioPlayer); - audioPlayer.addRoute(`bgms.${bgm}`, route); - } - } - }); -} diff --git a/src/module/audio/decoder.ts b/src/module/audio/decoder.ts index 537abc9..791bc38 100644 --- a/src/module/audio/decoder.ts +++ b/src/module/audio/decoder.ts @@ -1,12 +1,12 @@ -import { OggVorbisDecoder } from '@wasm-audio-decoders/ogg-vorbis'; +import { OggVorbisDecoderWebWorker } from '@wasm-audio-decoders/ogg-vorbis'; import { IAudioDecodeData, IAudioDecoder } from './source'; -import { OggOpusDecoder } from 'ogg-opus-decoder'; +import { OggOpusDecoderWebWorker } from 'ogg-opus-decoder'; export class VorbisDecoder implements IAudioDecoder { - decoder?: OggVorbisDecoder; + decoder?: OggVorbisDecoderWebWorker; async create(): Promise { - this.decoder = new OggVorbisDecoder(); + this.decoder = new OggVorbisDecoderWebWorker(); await this.decoder.ready; } @@ -19,15 +19,15 @@ export class VorbisDecoder implements IAudioDecoder { } async flush(): Promise { - return await this.decoder?.flush(); + return this.decoder?.flush(); } } export class OpusDecoder implements IAudioDecoder { - decoder?: OggOpusDecoder; + decoder?: OggOpusDecoderWebWorker; async create(): Promise { - this.decoder = new OggOpusDecoder(); + this.decoder = new OggOpusDecoderWebWorker(); await this.decoder.ready; } diff --git a/src/module/audio/index.ts b/src/module/audio/index.ts index a5ac962..553cec5 100644 --- a/src/module/audio/index.ts +++ b/src/module/audio/index.ts @@ -1,4 +1,4 @@ -import { loadAllBgm } from './bgmLoader'; +import { loadAllBgm } from './bgm'; import { OpusDecoder, VorbisDecoder } from './decoder'; import { AudioStreamSource } from './source'; import { AudioType } from './support'; @@ -11,4 +11,5 @@ export * from './support'; export * from './effect'; export * from './player'; export * from './source'; -export * from './bgmLoader'; +export * from './bgm'; +export * from './decoder'; diff --git a/src/module/audio/player.ts b/src/module/audio/player.ts index abc26b1..ae86d3b 100644 --- a/src/module/audio/player.ts +++ b/src/module/audio/player.ts @@ -193,6 +193,14 @@ export class AudioPlayer extends EventEmitter { return this.audioRoutes.get(id); } + /** + * 移除一个音频播放路由 + * @param id 要移除的播放路由的名称 + */ + removeRoute(id: string) { + this.audioRoutes.delete(id); + } + /** * 播放音频 * @param id 音频名称 @@ -272,6 +280,14 @@ export class AudioPlayer extends EventEmitter { } } +export const enum AudioStatus { + Playing, + Pausing, + Paused, + Stoping, + Stoped +} + type AudioStartHook = (route: AudioRoute) => void; type AudioEndHook = (time: number, route: AudioRoute) => void; @@ -295,11 +311,18 @@ export class AudioRoute /** 结束时长,当音频暂停或停止时,会经过这么长时间之后才真正终止播放,期间可以做音频淡入淡出等效果 */ endTime: number = 0; - /** 是否已暂停,注意停止播放是不算暂停的 */ - paused: boolean = false; + /** 当前播放状态 */ + status: AudioStatus = AudioStatus.Stoped; /** 暂停时刻 */ private pauseTime: number = 0; + private shouldStop: boolean = false; + /** + * 每次暂停或停止时自增,用于判断当前正在处理的情况。 + * 假如暂停后很快播放,然后很快暂停,那么需要根据这个来判断实际是否应该执行暂停后操作 + */ + stopIdentifier: number = 0; + private audioStartHook?: AudioStartHook; private audioEndHook?: AudioEndHook; @@ -341,16 +364,18 @@ export class AudioRoute * @param when 从音频的什么时候开始播放,单位秒 */ play(when: number = 0) { - if (this.source.playing) return; + if (this.status === AudioStatus.Playing) return; this.link(); if (this.effectRoute.length > 0) { const first = this.effectRoute[0]; this.source.connect(first); + const last = this.effectRoute.at(-1)!; + last.connect({ input: this.player.getDestination() }); } else { this.source.connect({ input: this.player.getDestination() }); } this.source.play(when); - this.paused = false; + this.status = AudioStatus.Playing; this.pauseTime = 0; this.audioStartHook?.(this); this.startAllEffect(); @@ -361,29 +386,55 @@ export class AudioRoute * 暂停音频播放 */ async pause() { - if (this.paused || !this.source.playing) return; + if (this.status !== AudioStatus.Playing) return; + this.status = AudioStatus.Pausing; + this.stopIdentifier++; + const identifier = this.stopIdentifier; if (this.audioEndHook) { this.audioEndHook(this.endTime, this); await sleep(this.endTime); } + if ( + this.status !== AudioStatus.Pausing || + this.stopIdentifier !== identifier + ) { + return; + } const time = this.source.stop(); this.pauseTime = time; - this.paused = true; - this.endAllEffect(); - this.emit('pause'); + if (this.shouldStop) { + this.status = AudioStatus.Stoped; + this.endAllEffect(); + this.emit('stop'); + this.shouldStop = false; + } else { + this.status = AudioStatus.Paused; + this.endAllEffect(); + this.emit('pause'); + } } /** * 继续音频播放 */ resume() { - if (this.source.playing) return; - if (this.paused) { + if (this.status === AudioStatus.Playing) return; + if ( + this.status === AudioStatus.Pausing || + this.status === AudioStatus.Stoping + ) { + console.log(1); + + this.audioStartHook?.(this); + this.emit('resume'); + return; + } + if (this.status === AudioStatus.Paused) { this.play(this.pauseTime); } else { this.play(0); } - this.paused = false; + this.status = AudioStatus.Playing; this.pauseTime = 0; this.audioStartHook?.(this); this.startAllEffect(); @@ -394,13 +445,27 @@ export class AudioRoute * 停止音频播放 */ async stop() { - if (!this.source.playing) return; + if (this.status !== AudioStatus.Playing) { + if (this.status === AudioStatus.Pausing) { + this.shouldStop = true; + } + return; + } + this.status = AudioStatus.Stoping; + this.stopIdentifier++; + const identifier = this.stopIdentifier; if (this.audioEndHook) { this.audioEndHook(this.endTime, this); await sleep(this.endTime); } + if ( + this.status !== AudioStatus.Stoping || + this.stopIdentifier !== identifier + ) { + return; + } this.source.stop(); - this.paused = false; + this.status = AudioStatus.Stoped; this.pauseTime = 0; this.endAllEffect(); this.emit('stop'); @@ -473,3 +538,4 @@ export class AudioRoute } export const audioPlayer = new AudioPlayer(); +// window.audioPlayer = audioPlayer; diff --git a/src/module/fallback/audio.ts b/src/module/fallback/audio.ts new file mode 100644 index 0000000..e5600a1 --- /dev/null +++ b/src/module/fallback/audio.ts @@ -0,0 +1,40 @@ +import { Patch, PatchClass } from '@/common/patch'; +import { bgmController } from '../audio'; +import { mainSetting } from '@/core/main/setting'; + +export function patchAudio() { + const patch = new Patch(PatchClass.Control); + + const play = (bgm: BgmIds, when?: number) => { + bgmController.play(bgm, when); + }; + const pause = () => { + bgmController.pause(); + }; + + patch.add('playBgm', function (bgm, startTime) { + play(bgm, startTime); + }); + patch.add('pauseBgm', function () { + pause(); + }); + patch.add('resumeBgm', function () { + bgmController.resume(); + }); + patch.add('checkBgm', function () { + if (bgmController.playing) return; + if (mainSetting.getValue('audio.bgmEnabled')) { + if (bgmController.playingBgm) { + bgmController.play(bgmController.playingBgm); + } else { + play(main.startBgm, 0); + } + } else { + pause(); + } + }); + patch.add('triggerBgm', function () { + if (bgmController.playing) bgmController.pause(); + else bgmController.resume(); + }); +} diff --git a/src/module/fallback/index.ts b/src/module/fallback/index.ts new file mode 100644 index 0000000..be00871 --- /dev/null +++ b/src/module/fallback/index.ts @@ -0,0 +1,11 @@ +import { Patch } from '@/common/patch'; +import { patchAudio } from './audio'; + +patchAudio(); + +export function patchAll() { + const loading = Mota.require('var', 'loading'); + loading.once('coreInit', () => { + Patch.patchAll(); + }); +} diff --git a/src/module/index.ts b/src/module/index.ts index dfb6154..71afe7a 100644 --- a/src/module/index.ts +++ b/src/module/index.ts @@ -1,7 +1,9 @@ +import { patchAll } from './fallback'; import { controller } from './weather'; import { RainWeather } from './weather/rain'; import { WeatherController } from './weather/weather'; +patchAll(); Mota.register('module', 'Weather', { controller, WeatherController, diff --git a/src/plugin/chase/chase1.ts b/src/plugin/chase/chase1.ts index 58e9dab..97aebfd 100644 --- a/src/plugin/chase/chase1.ts +++ b/src/plugin/chase/chase1.ts @@ -5,8 +5,8 @@ import { Camera, CameraAnimation, ICameraScale } from '@/core/render/camera'; import { LayerGroup } from '@/core/render/preset/layer'; import { MotaRenderer } from '@/core/render/render'; import { Sprite } from '@/core/render/sprite'; -import { bgm } from '@/core/audio/bgm'; import { PointEffect, PointEffectType } from '../fx/pointShader'; +import { bgmController } from '@/module'; const path: Partial> = { MT16: [ @@ -261,11 +261,12 @@ function initFromSave(chase: Chase) { } function playAudio(from: number, chase: Chase) { - bgm.changeTo('escape.mp3', from); - bgm.blockChange(); + const playing = bgmController.playingBgm; + bgmController.play('escape.mp3', from); + bgmController.blockChange(); chase.on('end', () => { - bgm.unblockChange(); - bgm.undo(); + bgmController.unblockChange(); + if (playing) bgmController.play(playing); }); } diff --git a/src/plugin/fallback.ts b/src/plugin/fallback.ts index b68bbb8..d113d4e 100644 --- a/src/plugin/fallback.ts +++ b/src/plugin/fallback.ts @@ -86,7 +86,7 @@ class Change extends RenderItem { protected render( canvas: MotaOffscreenCanvas2D, - transform: Transform + _transform: Transform ): void { if (this.backAlpha === 0) return; const ctx = canvas.ctx; diff --git a/src/ui/start.vue b/src/ui/start.vue index ec9d6b1..fe2fca4 100644 --- a/src/ui/start.vue +++ b/src/ui/start.vue @@ -72,8 +72,8 @@ import { gameKey } from '@/core/main/custom/hotkey'; import { mainUi } from '@/core/main/init/ui'; import { CustomToolbar } from '@/core/main/custom/toolbar'; import { mainSetting } from '@/core/main/setting'; -import { bgm as mainBgm } from '@/core/audio/bgm'; import { mat4 } from 'gl-matrix'; +import { bgmController } from '@/module'; const props = defineProps<{ num: number; @@ -328,7 +328,7 @@ onMounted(async () => { resize(); soundChecked.value = mainSetting.getValue('audio.bgmEnabled', true); - mainBgm.changeTo('title.mp3'); + bgmController.play('title.mp3'); start.style.opacity = '1'; if (played) { @@ -426,7 +426,8 @@ onUnmounted(() => { ); background-clip: text; -webkit-background-clip: text; - text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.5), + text-shadow: + 1px 1px 4px rgba(0, 0, 0, 0.5), -1px -1px 3px rgba(255, 255, 255, 0.3), 5px 5px 5px rgba(0, 0, 0, 0.4); filter: brightness(1.8); @@ -449,14 +450,17 @@ onUnmounted(() => { position: absolute; opacity: 0; animation: cursor 2.5s linear 0s infinite normal running; - transition: left 0.4s ease-out, top 0.4s ease-out, + transition: + left 0.4s ease-out, + top 0.4s ease-out, opacity 1.5s ease-out; } .start-button { position: relative; font: bold 1.5em 'normal'; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.4), + text-shadow: + 1px 1px 2px rgba(0, 0, 0, 0.4), 0px 0px 1px rgba(255, 255, 255, 0.3); background-clip: text; -webkit-background-clip: text;