refactor: bgm控制器

This commit is contained in:
unanmed 2025-01-18 21:54:24 +08:00
parent a058dfda4a
commit 1ef551799b
20 changed files with 598 additions and 428 deletions

View File

@ -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();

View File

@ -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);

128
src/common/patch.ts Normal file
View File

@ -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<DataCore, 'main'>;
[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<T extends PatchClass> {
private static patchList: Set<Patch<PatchClass>> = new Set();
private static patched: Partial<Record<PatchClass, Set<string>>> = {};
private patches: Map<string, (...params: any[]) => any> = new Map();
constructor(public readonly patchClass: T) {
Patch.patchList.add(this);
}
/**
*
* @param key
* @param patch
*/
add<
K extends Exclude<
SelectKey<PatchList[T], (...params: any[]) => 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<PatchClass>) {
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);
}
}

View File

@ -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<HTMLAudioElement>
implements Undoable<BgmIds>
{
/** 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<BgmIds, AnimatingBgm> = 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();
}
/**
* bgmpreventDefault来阻止渐变
* 使
*
* @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;
}
}
/**
* bgmchangeBgm事件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();

View File

@ -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<Record<AllIdsOf<'autotile'>, HTMLImageElement>> =
{};

View File

@ -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);

View File

@ -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<T extends number | boolean>(
key: string,
n: T,
o: T
_o: T
) {
if (key === 'fullscreen') {
// 全屏
@ -369,7 +369,7 @@ function handleScreenSetting<T extends number | boolean>(
function handleActionSetting<T extends number | boolean>(
key: string,
n: T,
o: T
_o: T
) {
if (key === 'autoSkill') {
// 自动切换技能
@ -382,13 +382,13 @@ function handleActionSetting<T extends number | boolean>(
function handleAudioSetting<T extends number | boolean>(
key: string,
n: T,
o: T
_o: T
) {
if (key === 'bgmEnabled') {
bgm.disable = !n;
if (core.isPlaying()) core.checkBgm();
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') {

View File

@ -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."
}

View File

@ -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<boolean>;
@ -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;
}
}

252
src/module/audio/bgm.ts Normal file
View File

@ -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<BgmControllerEvent> {
/** bgm音频名称的前缀 */
prefix: string = 'bgms.';
/** 每个 bgm 的音量控制器 */
readonly gain: Map<T, BgmVolume> = 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<BgmIds>(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);
}
});
}

View File

@ -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);
}
}
});
}

View File

@ -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<void> {
this.decoder = new OggVorbisDecoder();
this.decoder = new OggVorbisDecoderWebWorker();
await this.decoder.ready;
}
@ -19,15 +19,15 @@ export class VorbisDecoder implements IAudioDecoder {
}
async flush(): Promise<IAudioDecodeData | undefined> {
return await this.decoder?.flush();
return this.decoder?.flush();
}
}
export class OpusDecoder implements IAudioDecoder {
decoder?: OggOpusDecoder;
decoder?: OggOpusDecoderWebWorker;
async create(): Promise<void> {
this.decoder = new OggOpusDecoder();
this.decoder = new OggOpusDecoderWebWorker();
await this.decoder.ready;
}

View File

@ -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';

View File

@ -193,6 +193,14 @@ export class AudioPlayer extends EventEmitter<AudioPlayerEvent> {
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<AudioPlayerEvent> {
}
}
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;

View File

@ -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();
});
}

View File

@ -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();
});
}

View File

@ -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,

View File

@ -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<Record<FloorIds, LocArr[]>> = {
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);
});
}

View File

@ -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;

View File

@ -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;