From c1378ea24b70f34975bf6a771e68f2a920bc5546 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sun, 4 Feb 2024 15:19:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B8=90=E5=8F=98=E5=88=87=E6=AD=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/libs/control.js | 2 +- src/core/audio/audio.ts | 5 +- src/core/audio/bgm.ts | 242 +++++++++++++++++++++++++++++----- src/core/audio/sound.ts | 16 ++- src/core/interface.ts | 14 ++ src/core/loader/controller.ts | 18 ++- src/core/loader/resource.ts | 3 +- 7 files changed, 252 insertions(+), 48 deletions(-) create mode 100644 src/core/interface.ts diff --git a/public/libs/control.js b/public/libs/control.js index 1f065bb..c48dd23 100644 --- a/public/libs/control.js +++ b/public/libs/control.js @@ -3345,7 +3345,7 @@ control.prototype.screenFlash = function ( control.prototype.playBgm = function (bgm, startTime) { bgm = core.getMappedName(bgm); if (main.mode !== 'play') return; - Mota.require('var', 'bgm').play(bgm, startTime); + Mota.require('var', 'bgm').changeTo(bgm, startTime); }; ////// 暂停背景音乐的播放 ////// diff --git a/src/core/audio/audio.ts b/src/core/audio/audio.ts index a03ddb0..35eca82 100644 --- a/src/core/audio/audio.ts +++ b/src/core/audio/audio.ts @@ -13,10 +13,7 @@ interface AudioPlayerEvent extends EmitableEvent { end: (node: AudioBufferSourceNode) => void; } -export type AudioParamOf = Record< - SelectKey, - number ->; +export type AudioParamOf = Record, number>; export class AudioPlayer extends EventEmitter { static ac: AudioContext = ac; diff --git a/src/core/audio/bgm.ts b/src/core/audio/bgm.ts index 900d198..2887e91 100644 --- a/src/core/audio/bgm.ts +++ b/src/core/audio/bgm.ts @@ -1,13 +1,39 @@ -import { has } from '@/plugin/utils'; +import { Animation, TimingFn, Transition, linear, sleep } from 'mutate-animate'; +import { Undoable } from '../interface'; import { ResourceController } from '../loader/controller'; +import { has } from '@/plugin/utils'; -export class BgmController extends ResourceController { - playing?: BgmIds; - lastBgm?: BgmIds; +interface AnimatingBgm { + end: () => void; + ani: Transition; + timeout: number; + currentTime: 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; + + private transitionData: Map = new Map(); /** * 添加一个bgm - * @param uri bgm的uri + * @param uri bgm的`uri`,由于bgm是一类资源,因此`uri`为`bgms.xxx`的形式 * @param data bgm音频元素 */ add(uri: string, data: HTMLAudioElement) { @@ -18,25 +44,6 @@ export class BgmController extends ResourceController { data.loop = true; } - /** - * 切换bgm - * @param id bgm的id - */ - play(id: BgmIds, when: number = 0) { - if (this.playing === id) return; - this.pause(); - if (core.musicStatus.bgmStatus) { - const bgm = this.get(id); - bgm.currentTime = when; - bgm.volume = core.musicStatus.userVolume; - bgm.play(); - this.playing = id; - } else { - delete this.playing; - } - this.lastBgm = id; - } - /** * 加载一个bgm * @param id 要加载的bgm @@ -47,28 +54,191 @@ export class BgmController extends ResourceController { } /** - * 停止当前的bgm播放 + * 切换bgm,具有渐变效果,可以通过监听切换事件,同时调用preventDefault来阻止渐变, + * 并使用自己的切歌程序。阻止后,不会将切换的歌曲加入播放栈,也不会进行切歌, + * 所有的切歌操作均由你自己的程序执行 + * @param id 要切换至的bgm + * @param when 切换至的歌从什么时候开始播放,默认-1,表示不改变,整数表示设置为目标值 */ - pause() { - if (!has(this.playing)) return; - const bgm = this.get(this.playing); - bgm.pause(); - delete this.playing; + changeTo(id: BgmIds, when: number = -1, noStack: boolean = false) { + let prevent = false; + const preventDefault = () => { + prevent = true; + }; + const ev = { preventDefault }; + + this.emit('changeBgm', ev, id, this.now); + + if (prevent) return; + + this.setTransitionAnimate(id, 1); + if (this.now) this.setTransitionAnimate(this.now, 0, when); + + if (!noStack) { + if (this.now) this.stack.push(this.now); + this.redoStack = []; + } + this.now = id; } /** - * 继续上一个BGM的播放 + * 暂停当前bgm的播放,继续播放时将会延续暂停的时刻,同样可以使用preventDefault使用自己的暂停程序 + * @param transition 是否使用渐变效果,默认使用 */ - resume() { - if (has(this.playing) || !this.lastBgm) return; - const bgm = this.get(this.lastBgm); - bgm.play(); - this.playing = this.lastBgm; + pause(transition: boolean = true) { + if (!this.now) return; + let prevent = false; + const preventDefault = () => { + prevent = true; + }; + const ev = { preventDefault }; + + this.emit('pause', ev, this.now); + + if (prevent) return; + + if (transition) this.setTransitionAnimate(this.now, 0); + else this.get(this.now).pause(); } + /** + * 继续当前bgm的播放,从上一次暂停的时刻开始播放,同样可以使用preventDefault使用自己的播放程序 + * @param transition 是否使用渐变效果,默认使用 + */ + resume(transition: boolean = true) { + if (!this.now) return; + let prevent = false; + const preventDefault = () => { + prevent = true; + }; + const ev = { preventDefault }; + + this.emit('pause', ev, this.now); + + if (prevent) return; + + if (transition) this.setTransitionAnimate(this.now, 1); + else this.get(this.now).play(); + } + + /** + * 播放bgm,不进行渐变操作,效果为没有渐变的切歌 + * @param id 要播放的bgm + * @param when 从bgm的何时开始播放 + */ + play(id: BgmIds, when: number = 0, noStack: boolean = false) { + 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 (!noStack) { + if (this.now) this.stack.push(this.now); + this.redoStack = []; + } + this.now = id; + } + + /** + * 撤销当前播放,改为播放前一个bgm + */ + undo(transition: boolean = true, when: number = 0) { + 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.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 (ani.value.volume === 0) { + bgm.pause(); + } else { + bgm.volume = ani.value.volume * this.volume; + } + this.transitionData.delete(id); + }; + tran = { + end, + ani: ani, + timeout: -1, + currentTime: bgm.currentTime + }; + 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.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/audio/sound.ts b/src/core/audio/sound.ts index 0d54ea3..4703512 100644 --- a/src/core/audio/sound.ts +++ b/src/core/audio/sound.ts @@ -6,6 +6,7 @@ import { ResourceController } from '../loader/controller'; // todo: 立体声,可设置音源位置 type Panner = AudioParamOf; +type Listener = AudioParamOf; export class SoundEffect extends AudioPlayer { static playIndex = 0; @@ -35,7 +36,7 @@ export class SoundEffect extends AudioPlayer { return this._stereo; } - constructor(data: ArrayBuffer, stereo: boolean = false) { + constructor(data: ArrayBuffer, stereo: boolean = true) { super(data); this.on('end', node => { @@ -62,7 +63,7 @@ export class SoundEffect extends AudioPlayer { * ``` * @param stereo 是否启用立体声 */ - protected initAudio(stereo: boolean = false) { + protected initAudio(stereo: boolean = true) { const channel = this.buffer?.numberOfChannels; const ac = AudioPlayer.ac; if (!channel) return; @@ -125,13 +126,18 @@ export class SoundEffect extends AudioPlayer { /** * 设置立体声信息 - * @param panner 立体声信息 + * @param source 立体声声源位置与朝向 + * @param listener 听者的位置、头顶方向、面朝方向 */ - setPanner(panner: Partial) { + setPanner(source: Partial, listener: Partial) { if (!this.panner) return; - for (const [key, value] of Object.entries(panner)) { + for (const [key, value] of Object.entries(source)) { this.panner[key as keyof Panner].value = value; } + const l = AudioPlayer.ac.listener; + for (const [key, value] of Object.entries(listener)) { + l[key as keyof Listener].value = value; + } } } diff --git a/src/core/interface.ts b/src/core/interface.ts new file mode 100644 index 0000000..e96a5db --- /dev/null +++ b/src/core/interface.ts @@ -0,0 +1,14 @@ +export interface Undoable { + stack: T[]; + redoStack: T[]; + + /** + * 撤销 + */ + undo(): T | undefined; + + /** + * 重做 + */ + redo(): T | undefined; +} diff --git a/src/core/loader/controller.ts b/src/core/loader/controller.ts index d3b0049..d85baf1 100644 --- a/src/core/loader/controller.ts +++ b/src/core/loader/controller.ts @@ -1,4 +1,14 @@ -export abstract class ResourceController { +import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; + +interface ResourceControllerEvent extends EmitableEvent { + add: (uri: string, data: D) => void; + delete: (uri: string, content: T) => void; +} + +export abstract class ResourceController< + D, + T = D +> extends EventEmitter { list: Record = {}; /** @@ -8,7 +18,13 @@ export abstract class ResourceController { */ abstract add(uri: string, data: D): void; + /** + * 删除一个资源 + * @param uri 要删除的资源的uri + */ remove(uri: string) { + const content = this.list[uri]; delete this.list[uri]; + this.emit(uri, content); } } diff --git a/src/core/loader/resource.ts b/src/core/loader/resource.ts index 7630a84..98ca1af 100644 --- a/src/core/loader/resource.ts +++ b/src/core/loader/resource.ts @@ -4,6 +4,7 @@ import { ensureArray } from '@/plugin/utils'; import { has } from '@/plugin/utils'; import JSZip from 'jszip'; import { EmitableEvent, EventEmitter } from '../common/eventEmitter'; +import { bgm } from '../audio/bgm'; // todo: 应当用register去注册资源类型,然后进行分块处理 @@ -53,7 +54,7 @@ export class Resource< protected onLoadStart(v?: ResourceData[T]) { if (this.format === 'bgm') { // bgm 单独处理,因为它可以边播放边加载 - Mota.require('var', 'bgm').add(this.uri, v!); + bgm.add(this.uri, v!); } }