feat: 渐变切歌

This commit is contained in:
unanmed 2024-02-04 15:19:48 +08:00
parent db6983b4bd
commit c1378ea24b
7 changed files with 252 additions and 48 deletions

View File

@ -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);
}; };
////// 暂停背景音乐的播放 ////// ////// 暂停背景音乐的播放 //////

View File

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

View File

@ -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播放 * bgmpreventDefault来阻止渐变
* 使
*
* @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();

View File

@ -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
View File

@ -0,0 +1,14 @@
export interface Undoable<T> {
stack: T[];
redoStack: T[];
/**
*
*/
undo(): T | undefined;
/**
*
*/
redo(): T | undefined;
}

View File

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

View File

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