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) {
|
control.prototype.playBgm = function (bgm, startTime) {
|
||||||
bgm = core.getMappedName(bgm);
|
bgm = core.getMappedName(bgm);
|
||||||
if (main.mode !== 'play') return;
|
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;
|
end: (node: AudioBufferSourceNode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AudioParamOf<T extends AudioNode> = Record<
|
export type AudioParamOf<T> = Record<SelectKey<T, AudioParam>, number>;
|
||||||
SelectKey<T, AudioParam>,
|
|
||||||
number
|
|
||||||
>;
|
|
||||||
|
|
||||||
export class AudioPlayer extends EventEmitter<AudioPlayerEvent> {
|
export class AudioPlayer extends EventEmitter<AudioPlayerEvent> {
|
||||||
static ac: AudioContext = ac;
|
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 { ResourceController } from '../loader/controller';
|
||||||
|
import { has } from '@/plugin/utils';
|
||||||
|
|
||||||
export class BgmController extends ResourceController<HTMLAudioElement> {
|
interface AnimatingBgm {
|
||||||
playing?: BgmIds;
|
end: () => void;
|
||||||
lastBgm?: BgmIds;
|
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
|
* 添加一个bgm
|
||||||
* @param uri bgm的uri
|
* @param uri bgm的`uri`,由于bgm是一类资源,因此`uri`为`bgms.xxx`的形式
|
||||||
* @param data bgm音频元素
|
* @param data bgm音频元素
|
||||||
*/
|
*/
|
||||||
add(uri: string, data: HTMLAudioElement) {
|
add(uri: string, data: HTMLAudioElement) {
|
||||||
@ -18,25 +44,6 @@ export class BgmController extends ResourceController<HTMLAudioElement> {
|
|||||||
data.loop = true;
|
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
|
* 加载一个bgm
|
||||||
* @param 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() {
|
changeTo(id: BgmIds, when: number = -1, noStack: boolean = false) {
|
||||||
if (!has(this.playing)) return;
|
let prevent = false;
|
||||||
const bgm = this.get(this.playing);
|
const preventDefault = () => {
|
||||||
bgm.pause();
|
prevent = true;
|
||||||
delete this.playing;
|
};
|
||||||
|
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() {
|
pause(transition: boolean = true) {
|
||||||
if (has(this.playing) || !this.lastBgm) return;
|
if (!this.now) return;
|
||||||
const bgm = this.get(this.lastBgm);
|
let prevent = false;
|
||||||
bgm.play();
|
const preventDefault = () => {
|
||||||
this.playing = this.lastBgm;
|
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) {
|
get(id: BgmIds) {
|
||||||
return this.list[`bgms.${id}`];
|
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();
|
export const bgm = new BgmController();
|
||||||
|
@ -6,6 +6,7 @@ import { ResourceController } from '../loader/controller';
|
|||||||
// todo: 立体声,可设置音源位置
|
// todo: 立体声,可设置音源位置
|
||||||
|
|
||||||
type Panner = AudioParamOf<PannerNode>;
|
type Panner = AudioParamOf<PannerNode>;
|
||||||
|
type Listener = AudioParamOf<AudioListener>;
|
||||||
|
|
||||||
export class SoundEffect extends AudioPlayer {
|
export class SoundEffect extends AudioPlayer {
|
||||||
static playIndex = 0;
|
static playIndex = 0;
|
||||||
@ -35,7 +36,7 @@ export class SoundEffect extends AudioPlayer {
|
|||||||
return this._stereo;
|
return this._stereo;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(data: ArrayBuffer, stereo: boolean = false) {
|
constructor(data: ArrayBuffer, stereo: boolean = true) {
|
||||||
super(data);
|
super(data);
|
||||||
|
|
||||||
this.on('end', node => {
|
this.on('end', node => {
|
||||||
@ -62,7 +63,7 @@ export class SoundEffect extends AudioPlayer {
|
|||||||
* ```
|
* ```
|
||||||
* @param stereo 是否启用立体声
|
* @param stereo 是否启用立体声
|
||||||
*/
|
*/
|
||||||
protected initAudio(stereo: boolean = false) {
|
protected initAudio(stereo: boolean = true) {
|
||||||
const channel = this.buffer?.numberOfChannels;
|
const channel = this.buffer?.numberOfChannels;
|
||||||
const ac = AudioPlayer.ac;
|
const ac = AudioPlayer.ac;
|
||||||
if (!channel) return;
|
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;
|
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;
|
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> = {};
|
list: Record<string, T> = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -8,7 +18,13 @@ export abstract class ResourceController<D, T = D> {
|
|||||||
*/
|
*/
|
||||||
abstract add(uri: string, data: D): void;
|
abstract add(uri: string, data: D): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除一个资源
|
||||||
|
* @param uri 要删除的资源的uri
|
||||||
|
*/
|
||||||
remove(uri: string) {
|
remove(uri: string) {
|
||||||
|
const content = this.list[uri];
|
||||||
delete 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 { has } from '@/plugin/utils';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { EmitableEvent, EventEmitter } from '../common/eventEmitter';
|
import { EmitableEvent, EventEmitter } from '../common/eventEmitter';
|
||||||
|
import { bgm } from '../audio/bgm';
|
||||||
|
|
||||||
// todo: 应当用register去注册资源类型,然后进行分块处理
|
// todo: 应当用register去注册资源类型,然后进行分块处理
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ export class Resource<
|
|||||||
protected onLoadStart(v?: ResourceData[T]) {
|
protected onLoadStart(v?: ResourceData[T]) {
|
||||||
if (this.format === 'bgm') {
|
if (this.format === 'bgm') {
|
||||||
// bgm 单独处理,因为它可以边播放边加载
|
// bgm 单独处理,因为它可以边播放边加载
|
||||||
Mota.require('var', 'bgm').add(this.uri, v!);
|
bgm.add(this.uri, v!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user