mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-19 12:49:25 +08:00
feat: 渐变切歌
This commit is contained in:
parent
db6983b4bd
commit
c1378ea24b
@ -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);
|
||||
};
|
||||
|
||||
////// 暂停背景音乐的播放 //////
|
||||
|
@ -13,10 +13,7 @@ interface AudioPlayerEvent extends EmitableEvent {
|
||||
end: (node: AudioBufferSourceNode) => void;
|
||||
}
|
||||
|
||||
export type AudioParamOf<T extends AudioNode> = Record<
|
||||
SelectKey<T, AudioParam>,
|
||||
number
|
||||
>;
|
||||
export type AudioParamOf<T> = Record<SelectKey<T, AudioParam>, number>;
|
||||
|
||||
export class AudioPlayer extends EventEmitter<AudioPlayerEvent> {
|
||||
static ac: AudioContext = ac;
|
||||
|
@ -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<HTMLAudioElement> {
|
||||
playing?: BgmIds;
|
||||
lastBgm?: BgmIds;
|
||||
interface AnimatingBgm {
|
||||
end: () => void;
|
||||
ani: Transition;
|
||||
timeout: number;
|
||||
currentTime: 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;
|
||||
|
||||
private transitionData: Map<BgmIds, AnimatingBgm> = 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<HTMLAudioElement> {
|
||||
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<HTMLAudioElement> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止当前的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();
|
||||
|
@ -6,6 +6,7 @@ import { ResourceController } from '../loader/controller';
|
||||
// todo: 立体声,可设置音源位置
|
||||
|
||||
type Panner = AudioParamOf<PannerNode>;
|
||||
type Listener = AudioParamOf<AudioListener>;
|
||||
|
||||
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<Panner>) {
|
||||
setPanner(source: Partial<Panner>, listener: Partial<Listener>) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
14
src/core/interface.ts
Normal file
14
src/core/interface.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export interface Undoable<T> {
|
||||
stack: T[];
|
||||
redoStack: T[];
|
||||
|
||||
/**
|
||||
* 撤销
|
||||
*/
|
||||
undo(): T | undefined;
|
||||
|
||||
/**
|
||||
* 重做
|
||||
*/
|
||||
redo(): T | undefined;
|
||||
}
|
@ -1,4 +1,14 @@
|
||||
export abstract class ResourceController<D, T = D> {
|
||||
import { EmitableEvent, EventEmitter } from '../common/eventEmitter';
|
||||
|
||||
interface ResourceControllerEvent<D = any, T = D> extends EmitableEvent {
|
||||
add: (uri: string, data: D) => void;
|
||||
delete: (uri: string, content: T) => void;
|
||||
}
|
||||
|
||||
export abstract class ResourceController<
|
||||
D,
|
||||
T = D
|
||||
> extends EventEmitter<ResourceControllerEvent> {
|
||||
list: Record<string, T> = {};
|
||||
|
||||
/**
|
||||
@ -8,7 +18,13 @@ export abstract class ResourceController<D, T = D> {
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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!);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user