HumanBreak/src/module/audio/effect.ts

289 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { isNil } from 'lodash-es';
import { sleep } from 'mutate-animate';
export interface IAudioInput {
/** 输入节点 */
input: AudioNode;
}
export interface IAudioOutput {
/** 输出节点 */
output: AudioNode;
}
export abstract class AudioEffect implements IAudioInput, IAudioOutput {
/** 输出节点 */
abstract output: AudioNode;
/** 输入节点 */
abstract input: AudioNode;
constructor(public readonly ac: AudioContext) {}
/**
* 当音频播放结束时触发,可以用于节点结束后处理
*/
abstract end(): void;
/**
* 当音频开始播放时触发,可以用于节点初始化
*/
abstract start(): void;
/**
* 连接至其他效果器
* @param target 目标输入
* @param output 当前效果器输出通道
* @param input 目标效果器的输入通道
*/
connect(target: IAudioInput, output?: number, input?: number) {
this.output.connect(target.input, output, input);
}
/**
* 与其他效果器取消连接
* @param target 目标输入
* @param output 当前效果器输出通道
* @param input 目标效果器的输入通道
*/
disconnect(target?: IAudioInput, output?: number, input?: number) {
if (!target) {
if (!isNil(output)) {
this.output.disconnect(output);
} else {
this.output.disconnect();
}
} else {
if (!isNil(output)) {
if (!isNil(input)) {
this.output.disconnect(target.input, output, input);
} else {
this.output.disconnect(target.input, output);
}
} else {
this.output.disconnect(target.input);
}
}
}
}
export class StereoEffect extends AudioEffect {
output: PannerNode;
input: PannerNode;
constructor(ac: AudioContext) {
super(ac);
const panner = ac.createPanner();
this.input = panner;
this.output = panner;
}
/**
* 设置音频朝向x正方形水平向右y正方形垂直于地面向上z正方向垂直屏幕远离用户
* @param x 朝向x坐标
* @param y 朝向y坐标
* @param z 朝向z坐标
*/
setOrientation(x: number, y: number, z: number) {
this.output.orientationX.value = x;
this.output.orientationY.value = y;
this.output.orientationZ.value = z;
}
/**
* 设置音频位置x正方形水平向右y正方形垂直于地面向上z正方向垂直屏幕远离用户
* @param x 位置x坐标
* @param y 位置y坐标
* @param z 位置z坐标
*/
setPosition(x: number, y: number, z: number) {
this.output.positionX.value = x;
this.output.positionY.value = y;
this.output.positionZ.value = z;
}
end(): void {}
start(): void {}
}
export class VolumeEffect extends AudioEffect {
output: GainNode;
input: GainNode;
constructor(ac: AudioContext) {
super(ac);
const gain = ac.createGain();
this.input = gain;
this.output = gain;
}
/**
* 设置音量大小
* @param volume 音量大小
*/
setVolume(volume: number) {
this.output.gain.value = volume;
}
/**
* 获取音量大小
*/
getVolume(): number {
return this.output.gain.value;
}
end(): void {}
start(): void {}
}
export class ChannelVolumeEffect extends AudioEffect {
output: ChannelMergerNode;
input: ChannelSplitterNode;
/** 所有的音量控制节点 */
private readonly gain: GainNode[] = [];
constructor(ac: AudioContext) {
super(ac);
const splitter = ac.createChannelSplitter();
const merger = ac.createChannelMerger();
this.output = merger;
this.input = splitter;
for (let i = 0; i < 6; i++) {
const gain = ac.createGain();
splitter.connect(gain, i);
gain.connect(merger, 0, i);
this.gain.push(gain);
}
}
/**
* 设置某个声道的音量大小
* @param channel 要设置的声道可填0-5
* @param volume 这个声道的音量大小
*/
setVolume(channel: number, volume: number) {
if (!this.gain[channel]) return;
this.gain[channel].gain.value = volume;
}
/**
* 获取某个声道的音量大小可填0-5
* @param channel 要获取的声道
*/
getVolume(channel: number): number {
if (!this.gain[channel]) return 0;
return this.gain[channel].gain.value;
}
end(): void {}
start(): void {}
}
export class DelayEffect extends AudioEffect {
output: DelayNode;
input: DelayNode;
constructor(ac: AudioContext) {
super(ac);
const delay = ac.createDelay();
this.input = delay;
this.output = delay;
}
/**
* 设置延迟时长
* @param delay 延迟时长,单位秒
*/
setDelay(delay: number) {
this.output.delayTime.value = delay;
}
/**
* 获取延迟时长
*/
getDelay() {
return this.output.delayTime.value;
}
end(): void {}
start(): void {}
}
export class EchoEffect extends AudioEffect {
output: GainNode;
input: GainNode;
/** 延迟节点 */
private readonly delay: DelayNode;
/** 反馈增益节点 */
private readonly gainNode: GainNode;
/** 当前增益 */
private gain: number = 0.5;
/** 是否正在播放 */
private playing: boolean = false;
constructor(ac: AudioContext) {
super(ac);
const delay = ac.createDelay();
const gain = ac.createGain();
gain.gain.value = 0.5;
delay.delayTime.value = 0.05;
delay.connect(gain);
gain.connect(delay);
this.delay = delay;
this.gainNode = gain;
this.input = gain;
this.output = gain;
}
/**
* 设置回声反馈增益大小
* @param gain 增益大小,范围 0-1大于等于1的视为0.5小于0的视为0
*/
setFeedbackGain(gain: number) {
const resolved = gain >= 1 ? 0.5 : gain < 0 ? 0 : gain;
this.gain = resolved;
if (this.playing) this.gainNode.gain.value = resolved;
}
/**
* 设置回声间隔时长
* @param delay 回声时长,范围 0.01-Infinity小于0.01的视为0.01
*/
setEchoDelay(delay: number) {
const resolved = delay < 0.01 ? 0.01 : delay;
this.delay.delayTime.value = resolved;
}
/**
* 获取反馈节点增益
*/
getFeedbackGain() {
return this.gain;
}
/**
* 获取回声间隔时长
*/
getEchoDelay() {
return this.delay.delayTime.value;
}
end(): void {
this.playing = false;
const echoTime = Math.ceil(Math.log(0.001) / Math.log(this.gain)) + 10;
sleep(this.delay.delayTime.value * echoTime).then(() => {
if (!this.playing) this.gainNode.gain.value = 0;
});
}
start(): void {
this.playing = true;
this.gainNode.gain.value = this.gain;
}
}