@fluex/fluexgl-dsp 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -38
- package/lib/dist/console-codes.d.ts +22 -7
- package/lib/dist/console-codes.d.ts.map +1 -1
- package/lib/dist/console-codes.js +22 -7
- package/lib/dist/core/classes/AudioClip.d.ts +3 -1
- package/lib/dist/core/classes/AudioClip.d.ts.map +1 -1
- package/lib/dist/core/classes/AudioClip.js +23 -17
- package/lib/dist/core/classes/AudioClipPlayer.d.ts +10 -2
- package/lib/dist/core/classes/AudioClipPlayer.d.ts.map +1 -1
- package/lib/dist/core/classes/AudioClipPlayer.js +56 -2
- package/lib/dist/core/classes/AudioDevice.d.ts +2 -0
- package/lib/dist/core/classes/AudioDevice.d.ts.map +1 -1
- package/lib/dist/core/classes/AudioDevice.js +5 -0
- package/lib/dist/core/classes/Channel.d.ts +14 -4
- package/lib/dist/core/classes/Channel.d.ts.map +1 -1
- package/lib/dist/core/classes/Channel.js +93 -7
- package/lib/dist/core/classes/DpsPipeline.d.ts +1 -1
- package/lib/dist/core/classes/DpsPipeline.d.ts.map +1 -1
- package/lib/dist/core/classes/DpsPipeline.js +7 -3
- package/lib/dist/core/classes/Effector.d.ts +2 -2
- package/lib/dist/core/classes/Effector.d.ts.map +1 -1
- package/lib/dist/core/classes/Effector.js +1 -1
- package/lib/dist/core/classes/Master.d.ts +12 -0
- package/lib/dist/core/classes/Master.d.ts.map +1 -1
- package/lib/dist/core/classes/Master.js +80 -9
- package/lib/dist/effects/classes/Analyser.d.ts +16 -0
- package/lib/dist/effects/classes/Analyser.d.ts.map +1 -0
- package/lib/dist/effects/classes/Analyser.js +62 -0
- package/lib/dist/effects/classes/Chorus.d.ts +4 -3
- package/lib/dist/effects/classes/Chorus.d.ts.map +1 -1
- package/lib/dist/effects/classes/Chorus.js +21 -12
- package/lib/dist/effects/classes/clips/HardClip.d.ts +4 -3
- package/lib/dist/effects/classes/clips/HardClip.d.ts.map +1 -1
- package/lib/dist/effects/classes/clips/HardClip.js +15 -9
- package/lib/dist/effects/classes/clips/SoftClip.d.ts +4 -3
- package/lib/dist/effects/classes/clips/SoftClip.d.ts.map +1 -1
- package/lib/dist/effects/classes/clips/SoftClip.js +15 -9
- package/lib/dist/effects/classes/filters/LowPassFilter.d.ts +4 -3
- package/lib/dist/effects/classes/filters/LowPassFilter.d.ts.map +1 -1
- package/lib/dist/effects/classes/filters/LowPassFilter.js +23 -14
- package/lib/dist/effects/exports.d.ts +1 -0
- package/lib/dist/effects/exports.d.ts.map +1 -1
- package/lib/dist/effects/exports.js +1 -0
- package/lib/dist/index.d.ts +5 -4
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +13 -18
- package/lib/dist/typings.d.ts +13 -11
- package/lib/dist/typings.d.ts.map +1 -1
- package/lib/dist/typings.js +5 -0
- package/lib/dist/utilities/debugger.js +6 -6
- package/lib/dist/utilities/helpers.d.ts +2 -0
- package/lib/dist/utilities/helpers.d.ts.map +1 -1
- package/lib/dist/utilities/helpers.js +35 -12
- package/lib/dist/utilities/web-assembly.d.ts.map +1 -1
- package/lib/dist/utilities/web-assembly.js +6 -0
- package/lib/src/console-codes.ts +22 -7
- package/lib/src/core/classes/AudioClip.ts +28 -19
- package/lib/src/core/classes/AudioClipPlayer.ts +77 -8
- package/lib/src/core/classes/AudioDevice.ts +6 -1
- package/lib/src/core/classes/Channel.ts +126 -13
- package/lib/src/core/classes/DpsPipeline.ts +7 -3
- package/lib/src/core/classes/Effector.ts +3 -2
- package/lib/src/core/classes/Master.ts +101 -13
- package/lib/src/effects/classes/Analyser.ts +84 -0
- package/lib/src/effects/classes/Chorus.ts +22 -13
- package/lib/src/effects/classes/clips/HardClip.ts +17 -10
- package/lib/src/effects/classes/clips/SoftClip.ts +17 -10
- package/lib/src/effects/classes/filters/LowPassFilter.ts +28 -15
- package/lib/src/effects/exports.ts +2 -1
- package/lib/src/index.ts +27 -29
- package/lib/src/typings.ts +14 -14
- package/lib/src/utilities/debugger.ts +6 -6
- package/lib/src/utilities/helpers.ts +39 -15
- package/lib/src/utilities/web-assembly.ts +8 -0
- package/package.json +1 -1
- package/lib/dist/effects/classes/Filter.d.ts +0 -3
- package/lib/dist/effects/classes/Filter.d.ts.map +0 -1
- package/lib/dist/effects/classes/Filter.js +0 -2
- package/lib/dist/effects/classes/SoftClip.d.ts +0 -10
- package/lib/dist/effects/classes/SoftClip.d.ts.map +0 -1
- package/lib/dist/effects/classes/SoftClip.js +0 -39
- package/lib/dist/test/linkable-channels.d.ts +0 -2
- package/lib/dist/test/linkable-channels.d.ts.map +0 -1
- package/lib/dist/test/linkable-channels.js +0 -16
- package/lib/src/core/functions/rebuild-effect-chain.ts +0 -5
|
@@ -2,8 +2,9 @@ import { v4 } from "uuid";
|
|
|
2
2
|
|
|
3
3
|
import { AudioClip } from "./AudioClip";
|
|
4
4
|
import { Debug } from "../../utilities/debugger";
|
|
5
|
-
import { Master } from "./Master";
|
|
6
5
|
import { Channel } from "./Channel";
|
|
6
|
+
import { Master } from "./Master";
|
|
7
|
+
import { ErrorCodes } from "lib/src/console-codes";
|
|
7
8
|
|
|
8
9
|
export class AudioClipPlayer {
|
|
9
10
|
|
|
@@ -14,32 +15,100 @@ export class AudioClipPlayer {
|
|
|
14
15
|
public outputGainNode: GainNode | null = null;
|
|
15
16
|
|
|
16
17
|
public context: AudioContext | null = null;
|
|
17
|
-
public channel: Channel | null = null;
|
|
18
|
+
public channel: Channel | Master | null = null;
|
|
18
19
|
|
|
19
20
|
constructor(context: AudioContext) {
|
|
20
21
|
this.context = context;
|
|
21
22
|
this.outputGainNode = new GainNode(context);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
public AttachAudioClip(audioClip: AudioClip) {
|
|
25
|
+
public AttachAudioClip(audioClip: AudioClip): void {
|
|
25
26
|
|
|
26
27
|
if (this.audioClips.includes(audioClip)) return Debug.Error("Could not attach audio clip because it is already part of this channel", [
|
|
27
28
|
"Call .DetachAudioClip([clip AudioClip]) before attaching audio clip."
|
|
28
|
-
]);
|
|
29
|
+
], ErrorCodes.AUDIO_CLIP_ALREADY_ATTACHED);
|
|
29
30
|
|
|
30
31
|
audioClip.Initialize(this);
|
|
31
32
|
this.audioClips.push(audioClip);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
public
|
|
35
|
+
public DetachAudioClip(clip: AudioClip): void {
|
|
36
|
+
|
|
37
|
+
if (!this.audioClips.includes(clip)) return Debug.Error("Could not detach audio clip because it is not part of this channel", [
|
|
38
|
+
"Call .AttachAudioClip([clip AudioClip]) before detaching audio clip."
|
|
39
|
+
], ErrorCodes.AUDIO_CLIP_NOT_FOUND);
|
|
40
|
+
|
|
41
|
+
clip.Stop();
|
|
42
|
+
|
|
43
|
+
const self = this;
|
|
44
|
+
|
|
45
|
+
this.audioClips.forEach(function (_clip: AudioClip, index: number) {
|
|
46
|
+
if (clip.id === _clip.id)
|
|
47
|
+
return self.audioClips.splice(index, 1);
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public Send(channel: Channel | Master): void {
|
|
35
52
|
|
|
36
|
-
if(!this.outputGainNode || !channel.input) return Debug.Error("Could not send AudioClipPlayer signal to a channel.");
|
|
53
|
+
if (!this.outputGainNode || !channel.input) return Debug.Error("Could not send AudioClipPlayer signal to a channel/master.");
|
|
54
|
+
|
|
55
|
+
this.Unsend();
|
|
37
56
|
|
|
38
57
|
this.channel = channel;
|
|
39
58
|
this.outputGainNode.connect(channel.input);
|
|
40
59
|
}
|
|
41
60
|
|
|
42
|
-
public
|
|
61
|
+
public Unsend(): void {
|
|
62
|
+
|
|
63
|
+
if (!this.outputGainNode) return;
|
|
64
|
+
|
|
65
|
+
if (this.channel && this.channel.input) {
|
|
66
|
+
this.outputGainNode.disconnect(this.channel.input);
|
|
67
|
+
} else {
|
|
68
|
+
this.outputGainNode.disconnect();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.channel = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public SetVolume(value: number): void {
|
|
75
|
+
|
|
76
|
+
if (!this.outputGainNode) return;
|
|
77
|
+
|
|
78
|
+
if (value < 0) value = 0;
|
|
79
|
+
if (value > 1) value = 1;
|
|
80
|
+
|
|
81
|
+
this.outputGainNode.gain.value = value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public StopAll(): void {
|
|
85
|
+
|
|
86
|
+
this.audioClips.forEach(function (clip: AudioClip) {
|
|
87
|
+
clip.Stop();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public Dispose(): void {
|
|
92
|
+
|
|
93
|
+
this.StopAll();
|
|
94
|
+
this.Unsend();
|
|
95
|
+
this.outputGainNode?.disconnect();
|
|
96
|
+
|
|
97
|
+
this.audioClips = [];
|
|
98
|
+
this.outputGainNode = null;
|
|
99
|
+
this.channel = null;
|
|
100
|
+
this.context = null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public SetLabel(label: string): void {
|
|
43
104
|
this.label = label;
|
|
44
105
|
}
|
|
45
|
-
|
|
106
|
+
|
|
107
|
+
public get length(): number {
|
|
108
|
+
return this.audioClips.length;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public get volume(): number {
|
|
112
|
+
return this.outputGainNode?.gain.value ?? 0;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { v4 } from "uuid";
|
|
2
2
|
|
|
3
3
|
import { Master } from "./Master";
|
|
4
|
+
import { Channel } from "./Channel";
|
|
4
5
|
|
|
5
6
|
import { ErrorCodes } from "../../console-codes";
|
|
6
7
|
import { Debug } from "../../utilities/debugger";
|
|
@@ -15,7 +16,7 @@ export class AudioDevice {
|
|
|
15
16
|
public masterChannel: Master = new Master(this.context);
|
|
16
17
|
public masterChannels: Master[] = [];
|
|
17
18
|
|
|
18
|
-
constructor(public deviceInfo: MediaDeviceInfo) {
|
|
19
|
+
constructor(public deviceInfo: MediaDeviceInfo) {};
|
|
19
20
|
|
|
20
21
|
public GetMasterChannel(): Master {
|
|
21
22
|
return this.masterChannel;
|
|
@@ -42,4 +43,8 @@ export class AudioDevice {
|
|
|
42
43
|
public GetContext(): AudioContext {
|
|
43
44
|
return this.context;
|
|
44
45
|
}
|
|
46
|
+
|
|
47
|
+
public CreateChannel(label?: string): Channel {
|
|
48
|
+
return new Channel(this.context, label);
|
|
49
|
+
}
|
|
45
50
|
}
|
|
@@ -3,6 +3,8 @@ import { Debug } from "../../utilities/debugger";
|
|
|
3
3
|
import { AudioClipPlayer } from "./AudioClipPlayer";
|
|
4
4
|
import { AudioClip } from "./AudioClip";
|
|
5
5
|
import { Master } from "./Master";
|
|
6
|
+
import { Effector } from "./Effector";
|
|
7
|
+
import { ErrorCodes } from "../../console-codes";
|
|
6
8
|
|
|
7
9
|
export class Channel {
|
|
8
10
|
|
|
@@ -15,13 +17,15 @@ export class Channel {
|
|
|
15
17
|
public gainNode: GainNode | null = null;
|
|
16
18
|
public output: AudioNode | null = null;
|
|
17
19
|
|
|
20
|
+
public effects: Effector[] = [];
|
|
18
21
|
public context: AudioContext | null = null;
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
public sends: Channel[] = [];
|
|
24
|
+
public audioClipPlayer: AudioClipPlayer | null = null;
|
|
22
25
|
|
|
23
|
-
constructor(context: AudioContext) {
|
|
26
|
+
constructor(context: AudioContext, label?: string) {
|
|
24
27
|
this.context = context;
|
|
28
|
+
this.label = label ?? this.label;
|
|
25
29
|
|
|
26
30
|
this.disconnectAudioNodes(true);
|
|
27
31
|
|
|
@@ -41,6 +45,92 @@ export class Channel {
|
|
|
41
45
|
this.audioClipPlayer.Send(this);
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
private rebuildEffectChain(): void {
|
|
49
|
+
|
|
50
|
+
if (!this.input || !this.stereoPannerNode) {
|
|
51
|
+
Debug.Error("Could not rebuild effect chain because one or more required audio nodes are undefined.", [
|
|
52
|
+
`Channel id: ${this.id}.`,
|
|
53
|
+
`Input defined: ${!!this.input}.`,
|
|
54
|
+
`StereoPannerNode defined: ${!!this.stereoPannerNode}.`
|
|
55
|
+
]);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.input.disconnect();
|
|
60
|
+
|
|
61
|
+
for (const effect of this.effects)
|
|
62
|
+
effect.audioWorkletNode?.disconnect();
|
|
63
|
+
|
|
64
|
+
const activeEffects = this.effects.filter(function (e: Effector): boolean {
|
|
65
|
+
return !!e.audioWorkletNode;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (activeEffects.length === 0) {
|
|
69
|
+
this.input.connect(this.stereoPannerNode);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.input.connect(activeEffects[0].audioWorkletNode as AudioNode);
|
|
74
|
+
|
|
75
|
+
for (let i: number = 0; i < activeEffects.length - 1; i++) {
|
|
76
|
+
|
|
77
|
+
const current = activeEffects[i].audioWorkletNode as AudioNode;
|
|
78
|
+
const next = activeEffects[i + 1].audioWorkletNode as AudioNode;
|
|
79
|
+
|
|
80
|
+
current.connect(next);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
(activeEffects[activeEffects.length - 1].audioWorkletNode as AudioNode).connect(this.stereoPannerNode);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public AddEffect(effect: Effector): void {
|
|
87
|
+
|
|
88
|
+
if (!this.context) {
|
|
89
|
+
Debug.Error("Could not add effect because this channel has no AudioContext.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this.effects.includes(effect)) {
|
|
94
|
+
Debug.Error("Could not add effect because it is already part of this channel", [
|
|
95
|
+
"Call .RemoveEffect([effect Effector]) before adding effect."
|
|
96
|
+
], ErrorCodes.EFFECT_ALREADY_ATTACHED);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.effects.push(effect);
|
|
101
|
+
effect.InitializeOnAttachment(this.context);
|
|
102
|
+
this.rebuildEffectChain();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public AttachEffect(effect: Effector): void {
|
|
106
|
+
return this.AddEffect(effect);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public RemoveEffect(effect: Effector): void {
|
|
110
|
+
|
|
111
|
+
if (!this.effects.includes(effect)) {
|
|
112
|
+
Debug.Error("Could not remove effect, because it is not part of this channel.", [
|
|
113
|
+
"Call .AddEffect([effect Effector]) before removing effect."
|
|
114
|
+
], ErrorCodes.EFFECT_NOT_FOUND);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const self: Channel = this;
|
|
119
|
+
|
|
120
|
+
this.effects.forEach(function (_effect: Effector, index: number): void {
|
|
121
|
+
if (effect.id === _effect.id)
|
|
122
|
+
self.effects.splice(index, 1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
effect.audioWorkletNode?.disconnect();
|
|
126
|
+
|
|
127
|
+
this.rebuildEffectChain();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public DetachEffect(effect: Effector): void {
|
|
131
|
+
return this.RemoveEffect(effect);
|
|
132
|
+
}
|
|
133
|
+
|
|
44
134
|
private disconnectAudioNodes(gc?: boolean) {
|
|
45
135
|
|
|
46
136
|
this.input?.disconnect();
|
|
@@ -49,6 +139,10 @@ export class Channel {
|
|
|
49
139
|
this.gainNode?.disconnect();
|
|
50
140
|
this.output?.disconnect();
|
|
51
141
|
|
|
142
|
+
for (const effect of this.effects) {
|
|
143
|
+
effect.audioWorkletNode?.disconnect();
|
|
144
|
+
}
|
|
145
|
+
|
|
52
146
|
if (gc) {
|
|
53
147
|
this.input = null;
|
|
54
148
|
this.stereoPannerNode = null;
|
|
@@ -88,31 +182,32 @@ export class Channel {
|
|
|
88
182
|
|
|
89
183
|
public Send(channel: Channel | Master) {
|
|
90
184
|
|
|
91
|
-
if (channel instanceof Master)
|
|
185
|
+
if (channel instanceof Master)
|
|
186
|
+
return (channel as Master).AttachChannel(this);
|
|
92
187
|
|
|
93
188
|
if (channel.id === this.id) return Debug.Error("Could not link channel to itself.", [
|
|
94
189
|
`This channel id: ${this.id}`
|
|
95
|
-
]);
|
|
190
|
+
], ErrorCodes.CHANNEL_CANNOT_LINK_TO_ITSELF);
|
|
96
191
|
|
|
97
192
|
if (!this.isInitialized() || !channel.isInitialized()) return Debug.Error("Could not link channels because one (or both) channels are not initialized.", [
|
|
98
193
|
`This channel id: ${this.id} initialized: ${this.isInitialized()}`,
|
|
99
194
|
`Target channel id: ${channel.id} initialized: ${channel.isInitialized()}`
|
|
100
|
-
]);
|
|
195
|
+
], ErrorCodes.CHANNEL_NOT_INITIALIZED);
|
|
101
196
|
|
|
102
197
|
if (this.context !== channel.context) return Debug.Error("Could not link channels because they do not share the same AudioContext.", [
|
|
103
198
|
`This channel context: ${this.context ? "set" : "null"}`,
|
|
104
199
|
`Target channel context: ${channel.context ? "set" : "null"}`
|
|
105
|
-
]);
|
|
200
|
+
], ErrorCodes.CHANNEL_NOT_SAME_AUDIO_CONTEXT);
|
|
106
201
|
|
|
107
202
|
if (this.sends.includes(channel)) return Debug.Error("Could not link channels, because the given channel is already linked with this one.", [
|
|
108
203
|
`This channel id: ${this.id}`,
|
|
109
204
|
`Target channel id: ${channel.id}`
|
|
110
|
-
]);
|
|
205
|
+
], ErrorCodes.CHANNEL_ALREADY_LINKED);
|
|
111
206
|
|
|
112
207
|
if (channel.isReachable(this)) return Debug.Error("Could not link channels because it would create a feedback loop.", [
|
|
113
208
|
`This channel id: ${this.id}`,
|
|
114
209
|
`Target channel id: ${channel.id}`
|
|
115
|
-
]);
|
|
210
|
+
], ErrorCodes.CHANNEL_POSSIBLE_FEEDBACK_LOOP);
|
|
116
211
|
|
|
117
212
|
(this.output as AudioNode).connect(channel.input as AudioNode);
|
|
118
213
|
|
|
@@ -121,7 +216,7 @@ export class Channel {
|
|
|
121
216
|
|
|
122
217
|
public Unsend(channel: Channel | Master) {
|
|
123
218
|
|
|
124
|
-
if(channel instanceof Master)
|
|
219
|
+
if (channel instanceof Master)
|
|
125
220
|
return (channel as Master).DetachChannel(this);
|
|
126
221
|
|
|
127
222
|
const idx: number = this.sends.indexOf(channel);
|
|
@@ -141,16 +236,34 @@ export class Channel {
|
|
|
141
236
|
public UnsendToAllChannels() {
|
|
142
237
|
|
|
143
238
|
for (var i: number = 0; i < this.sends.length; i++) {
|
|
144
|
-
|
|
145
239
|
this.Unsend(this.sends[i]);
|
|
146
240
|
i--;
|
|
147
241
|
}
|
|
148
242
|
}
|
|
149
243
|
|
|
150
|
-
public AttachAudioClip(audioClip: AudioClip) {
|
|
244
|
+
public AttachAudioClip(audioClip: AudioClip): Channel {
|
|
151
245
|
|
|
152
|
-
if (!this.audioClipPlayer)
|
|
246
|
+
if (!this.audioClipPlayer) throw Error("Cannot not link AudioClip to this channel because this channel's AudioClipPlayer is undefined.");
|
|
153
247
|
|
|
154
248
|
this.audioClipPlayer.AttachAudioClip(audioClip);
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public Volume(volume?: number): number {
|
|
253
|
+
|
|
254
|
+
if(!this.context) throw new Error("Could not set volume on channel, because it's context is undefined.");
|
|
255
|
+
if(!this.gainNode) throw new Error("Could not set volume on channel, because it's GainNode is undefined.")
|
|
256
|
+
|
|
257
|
+
volume && this.gainNode.gain.setValueAtTime(volume, this.context.currentTime);
|
|
258
|
+
return volume ?? this.gainNode.gain.value;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public Pan(pan?: number): number {
|
|
262
|
+
|
|
263
|
+
if(!this.context) throw new Error("Could not set pan on channel, because it's context is undefined.");
|
|
264
|
+
if(!this.stereoPannerNode) throw new Error("Cannot set pan on channel, because it's StereoPannerNode is undefined.");
|
|
265
|
+
|
|
266
|
+
pan && this.stereoPannerNode.pan.setValueAtTime(pan, this.context.currentTime);
|
|
267
|
+
return pan ?? this.stereoPannerNode.pan.value;
|
|
155
268
|
}
|
|
156
269
|
}
|
|
@@ -7,6 +7,7 @@ import { ConstructProcessorWorklet, LoadWorkletOnAudioDevice } from "../../utili
|
|
|
7
7
|
|
|
8
8
|
import { ErrorCodes, WarningCodes } from "../../console-codes";
|
|
9
9
|
import { DspPipelineInitializationOptions } from "../../typings";
|
|
10
|
+
import { DSP } from "../../index";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Represents the digital signal processing (DSP) pipeline responsible for initializing
|
|
@@ -45,9 +46,12 @@ export class DspPipeline {
|
|
|
45
46
|
* @param options - Initialization options for the DSP pipeline.
|
|
46
47
|
* Must include valid paths to the WebAssembly and AudioWorklet files.
|
|
47
48
|
*/
|
|
48
|
-
constructor(options: DspPipelineInitializationOptions) {
|
|
49
|
-
this.pathToWasm =
|
|
50
|
-
this.pathToWorklet =
|
|
49
|
+
constructor({ pathToWasm, pathToWorklet, options }: DspPipelineInitializationOptions) {
|
|
50
|
+
this.pathToWasm = pathToWasm;
|
|
51
|
+
this.pathToWorklet = pathToWorklet;
|
|
52
|
+
|
|
53
|
+
// @ts-ignore
|
|
54
|
+
for(const key in options) DSP[key] = options[key];
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
/**
|
|
@@ -8,8 +8,9 @@ export abstract class Effector {
|
|
|
8
8
|
public id: string = v4();
|
|
9
9
|
public label: string | null = null;
|
|
10
10
|
public name: string = "Effector";
|
|
11
|
+
|
|
11
12
|
public audioWorkletNode: AudioWorkletNode | null = null;
|
|
12
|
-
public
|
|
13
|
+
public context: AudioContext | null = null;
|
|
13
14
|
|
|
14
15
|
private hasRegisteredMessageEventListener: boolean = false;
|
|
15
16
|
|
|
@@ -88,5 +89,5 @@ export abstract class Effector {
|
|
|
88
89
|
return this;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
public abstract InitializeOnAttachment(
|
|
92
|
+
public abstract InitializeOnAttachment(context: AudioContext): Promise<void>;
|
|
92
93
|
}
|
|
@@ -1,42 +1,121 @@
|
|
|
1
1
|
import { v4 } from "uuid";
|
|
2
2
|
|
|
3
3
|
import { Channel } from "./Channel";
|
|
4
|
-
|
|
5
4
|
import { Debug } from "../../utilities/debugger";
|
|
5
|
+
import { Effector } from "./Effector";
|
|
6
6
|
import { AudioClipPlayer } from "./AudioClipPlayer";
|
|
7
|
+
import { AudioClip } from "./AudioClip";
|
|
8
|
+
import { ErrorCodes } from "lib/src/console-codes";
|
|
7
9
|
|
|
8
10
|
export class Master {
|
|
9
11
|
|
|
10
12
|
public id: string = v4();
|
|
13
|
+
|
|
11
14
|
public channels: Channel[] = [];
|
|
15
|
+
public effects: Effector[] = [];
|
|
12
16
|
|
|
17
|
+
public input: GainNode | null = null;
|
|
13
18
|
public gainNode: GainNode | null = null;
|
|
14
19
|
public analyserNode: AnalyserNode | null = null;
|
|
15
|
-
|
|
16
20
|
public context: AudioContext | null = null;
|
|
21
|
+
public audioClipPlayer: AudioClipPlayer | null = null;
|
|
17
22
|
|
|
18
23
|
constructor(context: AudioContext) {
|
|
19
24
|
|
|
20
25
|
this.context = context;
|
|
21
26
|
|
|
27
|
+
this.disconnectAudioNodes();
|
|
28
|
+
|
|
29
|
+
this.audioClipPlayer = new AudioClipPlayer(context);
|
|
30
|
+
|
|
31
|
+
this.input = new GainNode(context);
|
|
22
32
|
this.gainNode = new GainNode(context);
|
|
23
33
|
this.analyserNode = new AnalyserNode(context);
|
|
24
34
|
|
|
35
|
+
this.input.connect(this.gainNode);
|
|
36
|
+
this.gainNode.connect(this.analyserNode);
|
|
37
|
+
this.analyserNode.connect(this.context.destination);
|
|
38
|
+
|
|
39
|
+
this.audioClipPlayer.Send(this);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private disconnectAudioNodes(): void {
|
|
43
|
+
|
|
44
|
+
if (this.input) this.input.disconnect();
|
|
45
|
+
if (this.gainNode) this.gainNode.disconnect();
|
|
46
|
+
if (this.analyserNode) this.analyserNode.disconnect();
|
|
47
|
+
|
|
48
|
+
this.effects.forEach(function (effect: Effector) {
|
|
49
|
+
if (effect.audioWorkletNode)
|
|
50
|
+
effect.audioWorkletNode.disconnect();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private rebuildEffectChain(): void {
|
|
55
|
+
|
|
56
|
+
if (!this.context) return;
|
|
57
|
+
if (!this.input || !this.gainNode || !this.analyserNode) return;
|
|
58
|
+
|
|
59
|
+
this.disconnectAudioNodes();
|
|
60
|
+
|
|
61
|
+
let currentNode: AudioNode = this.input;
|
|
62
|
+
|
|
63
|
+
this.effects.forEach(function (effect: Effector) {
|
|
64
|
+
|
|
65
|
+
if (!effect.audioWorkletNode) return;
|
|
66
|
+
|
|
67
|
+
currentNode.connect(effect.audioWorkletNode);
|
|
68
|
+
currentNode = effect.audioWorkletNode;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
currentNode.connect(this.gainNode);
|
|
72
|
+
|
|
25
73
|
this.gainNode.connect(this.analyserNode);
|
|
26
74
|
this.analyserNode.connect(this.context.destination);
|
|
27
75
|
}
|
|
28
76
|
|
|
77
|
+
public AttachEffect(effect: Effector): void {
|
|
78
|
+
|
|
79
|
+
if (this.effects.includes(effect)) return Debug.Error("Could not attach the effect because it is already part of this master channel.", [
|
|
80
|
+
"Call .DetachEffect([effect Effector]) before attaching the effect."
|
|
81
|
+
], ErrorCodes.EFFECT_ALREADY_ATTACHED);
|
|
82
|
+
|
|
83
|
+
if(!this.context) return this.rebuildEffectChain();
|
|
84
|
+
|
|
85
|
+
this.effects.push(effect);
|
|
86
|
+
effect.InitializeOnAttachment(this.context);
|
|
87
|
+
this.rebuildEffectChain();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public DetachEffect(effect: Effector): void {
|
|
91
|
+
|
|
92
|
+
if (!this.effects.includes(effect)) return Debug.Error("Could not detach the effect because it is not part of this master channel.", [
|
|
93
|
+
"Call .AttachEffect([effect Effector]) before detaching the effect."
|
|
94
|
+
], ErrorCodes.EFFECT_NOT_FOUND);
|
|
95
|
+
|
|
96
|
+
const self = this;
|
|
97
|
+
|
|
98
|
+
this.effects.forEach(function (_effect: Effector, index: number) {
|
|
99
|
+
if (effect.id === _effect.id) {
|
|
100
|
+
self.effects.splice(index, 1);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
this.rebuildEffectChain();
|
|
106
|
+
}
|
|
107
|
+
|
|
29
108
|
public AttachChannel(channel: Channel): void {
|
|
30
109
|
|
|
31
110
|
if (this.channels.includes(channel)) return Debug.Error("Could not attach the channel because it is already part of this master channel.", [
|
|
32
111
|
"Call .DetachChannel([channel Channel]) before attaching the channel."
|
|
33
|
-
]);
|
|
112
|
+
], ErrorCodes.CHANNEL_ALREADY_ATTACHED);
|
|
34
113
|
|
|
35
114
|
this.channels.push(channel);
|
|
36
115
|
|
|
37
|
-
if (channel.output && this.
|
|
38
|
-
channel.output.connect(this.
|
|
39
|
-
|
|
116
|
+
if (channel.output && this.input)
|
|
117
|
+
channel.output.connect(this.input);
|
|
118
|
+
|
|
40
119
|
return;
|
|
41
120
|
}
|
|
42
121
|
|
|
@@ -44,19 +123,28 @@ export class Master {
|
|
|
44
123
|
|
|
45
124
|
if (!this.channels.includes(channel)) return Debug.Error("Could not detach the channel because it is not part of this master channel.", [
|
|
46
125
|
"Call .AttachChannel([channel Channel]) before detaching the channel."
|
|
47
|
-
]);
|
|
126
|
+
], ErrorCodes.CHANNEL_NOT_FOUND);
|
|
48
127
|
|
|
49
|
-
if (channel.output && this.
|
|
50
|
-
channel.output.disconnect(this.
|
|
128
|
+
if (channel.output && this.input)
|
|
129
|
+
channel.output.disconnect(this.input);
|
|
51
130
|
|
|
52
131
|
const self = this;
|
|
53
132
|
|
|
54
133
|
this.channels.forEach(function (_channel: Channel, index: number) {
|
|
55
|
-
if (channel.id
|
|
134
|
+
if (channel.id !== _channel.id) return;
|
|
56
135
|
|
|
57
|
-
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
136
|
+
self.channels.splice(index, 1);
|
|
60
137
|
});
|
|
61
138
|
}
|
|
139
|
+
|
|
140
|
+
public HasAudioClipPlayer(): boolean {
|
|
141
|
+
return !!this.audioClipPlayer;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public AttachAudioClip(audioClip: AudioClip) {
|
|
145
|
+
|
|
146
|
+
if (!this.audioClipPlayer) return Debug.Error("Cannot not link AudioClip to this channel because this channel's AudioClipPlayer is undefined.");
|
|
147
|
+
|
|
148
|
+
this.audioClipPlayer.AttachAudioClip(audioClip);
|
|
149
|
+
}
|
|
62
150
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Effector } from "../../core/exports";
|
|
2
|
+
|
|
3
|
+
type AnalyserOptionKeys = keyof Omit<AnalyserOptions, "channelCount" | "channelCountMode" | "channelInterpretation">;
|
|
4
|
+
|
|
5
|
+
export class Analyser extends Effector {
|
|
6
|
+
|
|
7
|
+
public label: string | null = "Analyser";
|
|
8
|
+
public name: string = "Analyser";
|
|
9
|
+
|
|
10
|
+
public analyserNode: AnalyserNode | null = null;
|
|
11
|
+
|
|
12
|
+
public float32ArrayBuffer = new Float32Array();
|
|
13
|
+
public uint8ArrayBuffer = new Uint8Array();
|
|
14
|
+
|
|
15
|
+
private options: AnalyserOptions = {
|
|
16
|
+
fftSize: 32,
|
|
17
|
+
smoothingTimeConstant: 0.8,
|
|
18
|
+
minDecibels: -90,
|
|
19
|
+
maxDecibels: -10,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
constructor(options?: Partial<AnalyserOptions>) {
|
|
23
|
+
super();
|
|
24
|
+
|
|
25
|
+
if (options) this.SetOptions(options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public async InitializeOnAttachment(context: AudioContext): Promise<void> {
|
|
29
|
+
|
|
30
|
+
this.context = context;
|
|
31
|
+
this.analyserNode = new AnalyserNode(context, this.options);
|
|
32
|
+
|
|
33
|
+
this.audioWorkletNode = this.analyserNode as unknown as AudioWorkletNode;
|
|
34
|
+
this.allocateBuffers();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public SetOptions(options: Partial<AnalyserOptions>): void {
|
|
38
|
+
|
|
39
|
+
(Object.keys(options) as AnalyserOptionKeys[]).forEach((key) => {
|
|
40
|
+
const value = options[key];
|
|
41
|
+
|
|
42
|
+
if (typeof value === "undefined") return;
|
|
43
|
+
|
|
44
|
+
if (key === "fftSize" && value) {
|
|
45
|
+
const allowed = [32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768];
|
|
46
|
+
if (!allowed.includes(value)) return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
(this.options as any)[key] = value;
|
|
50
|
+
|
|
51
|
+
if (this.analyserNode) {
|
|
52
|
+
(this.analyserNode as any)[key] = value;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (options.fftSize && this.analyserNode) {
|
|
57
|
+
this.allocateBuffers();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public GetWaveformFloatData(): Float32Array | null {
|
|
62
|
+
|
|
63
|
+
if (!this.analyserNode) return null;
|
|
64
|
+
|
|
65
|
+
this.analyserNode.getFloatTimeDomainData(this.float32ArrayBuffer);
|
|
66
|
+
return this.float32ArrayBuffer;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public GetWaveformByteData(): Uint8Array | null {
|
|
70
|
+
|
|
71
|
+
if (!this.analyserNode) return null;
|
|
72
|
+
|
|
73
|
+
this.analyserNode.getByteTimeDomainData(this.uint8ArrayBuffer);
|
|
74
|
+
return this.uint8ArrayBuffer;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private allocateBuffers(): void {
|
|
78
|
+
|
|
79
|
+
if (!this.analyserNode) return;
|
|
80
|
+
|
|
81
|
+
this.float32ArrayBuffer = new Float32Array(this.analyserNode.fftSize);
|
|
82
|
+
this.uint8ArrayBuffer = new Uint8Array(this.analyserNode.fftSize);
|
|
83
|
+
}
|
|
84
|
+
}
|