@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.
Files changed (85) hide show
  1. package/README.md +16 -38
  2. package/lib/dist/console-codes.d.ts +22 -7
  3. package/lib/dist/console-codes.d.ts.map +1 -1
  4. package/lib/dist/console-codes.js +22 -7
  5. package/lib/dist/core/classes/AudioClip.d.ts +3 -1
  6. package/lib/dist/core/classes/AudioClip.d.ts.map +1 -1
  7. package/lib/dist/core/classes/AudioClip.js +23 -17
  8. package/lib/dist/core/classes/AudioClipPlayer.d.ts +10 -2
  9. package/lib/dist/core/classes/AudioClipPlayer.d.ts.map +1 -1
  10. package/lib/dist/core/classes/AudioClipPlayer.js +56 -2
  11. package/lib/dist/core/classes/AudioDevice.d.ts +2 -0
  12. package/lib/dist/core/classes/AudioDevice.d.ts.map +1 -1
  13. package/lib/dist/core/classes/AudioDevice.js +5 -0
  14. package/lib/dist/core/classes/Channel.d.ts +14 -4
  15. package/lib/dist/core/classes/Channel.d.ts.map +1 -1
  16. package/lib/dist/core/classes/Channel.js +93 -7
  17. package/lib/dist/core/classes/DpsPipeline.d.ts +1 -1
  18. package/lib/dist/core/classes/DpsPipeline.d.ts.map +1 -1
  19. package/lib/dist/core/classes/DpsPipeline.js +7 -3
  20. package/lib/dist/core/classes/Effector.d.ts +2 -2
  21. package/lib/dist/core/classes/Effector.d.ts.map +1 -1
  22. package/lib/dist/core/classes/Effector.js +1 -1
  23. package/lib/dist/core/classes/Master.d.ts +12 -0
  24. package/lib/dist/core/classes/Master.d.ts.map +1 -1
  25. package/lib/dist/core/classes/Master.js +80 -9
  26. package/lib/dist/effects/classes/Analyser.d.ts +16 -0
  27. package/lib/dist/effects/classes/Analyser.d.ts.map +1 -0
  28. package/lib/dist/effects/classes/Analyser.js +62 -0
  29. package/lib/dist/effects/classes/Chorus.d.ts +4 -3
  30. package/lib/dist/effects/classes/Chorus.d.ts.map +1 -1
  31. package/lib/dist/effects/classes/Chorus.js +21 -12
  32. package/lib/dist/effects/classes/clips/HardClip.d.ts +4 -3
  33. package/lib/dist/effects/classes/clips/HardClip.d.ts.map +1 -1
  34. package/lib/dist/effects/classes/clips/HardClip.js +15 -9
  35. package/lib/dist/effects/classes/clips/SoftClip.d.ts +4 -3
  36. package/lib/dist/effects/classes/clips/SoftClip.d.ts.map +1 -1
  37. package/lib/dist/effects/classes/clips/SoftClip.js +15 -9
  38. package/lib/dist/effects/classes/filters/LowPassFilter.d.ts +4 -3
  39. package/lib/dist/effects/classes/filters/LowPassFilter.d.ts.map +1 -1
  40. package/lib/dist/effects/classes/filters/LowPassFilter.js +23 -14
  41. package/lib/dist/effects/exports.d.ts +1 -0
  42. package/lib/dist/effects/exports.d.ts.map +1 -1
  43. package/lib/dist/effects/exports.js +1 -0
  44. package/lib/dist/index.d.ts +5 -4
  45. package/lib/dist/index.d.ts.map +1 -1
  46. package/lib/dist/index.js +13 -18
  47. package/lib/dist/typings.d.ts +13 -11
  48. package/lib/dist/typings.d.ts.map +1 -1
  49. package/lib/dist/typings.js +5 -0
  50. package/lib/dist/utilities/debugger.js +6 -6
  51. package/lib/dist/utilities/helpers.d.ts +2 -0
  52. package/lib/dist/utilities/helpers.d.ts.map +1 -1
  53. package/lib/dist/utilities/helpers.js +35 -12
  54. package/lib/dist/utilities/web-assembly.d.ts.map +1 -1
  55. package/lib/dist/utilities/web-assembly.js +6 -0
  56. package/lib/src/console-codes.ts +22 -7
  57. package/lib/src/core/classes/AudioClip.ts +28 -19
  58. package/lib/src/core/classes/AudioClipPlayer.ts +77 -8
  59. package/lib/src/core/classes/AudioDevice.ts +6 -1
  60. package/lib/src/core/classes/Channel.ts +126 -13
  61. package/lib/src/core/classes/DpsPipeline.ts +7 -3
  62. package/lib/src/core/classes/Effector.ts +3 -2
  63. package/lib/src/core/classes/Master.ts +101 -13
  64. package/lib/src/effects/classes/Analyser.ts +84 -0
  65. package/lib/src/effects/classes/Chorus.ts +22 -13
  66. package/lib/src/effects/classes/clips/HardClip.ts +17 -10
  67. package/lib/src/effects/classes/clips/SoftClip.ts +17 -10
  68. package/lib/src/effects/classes/filters/LowPassFilter.ts +28 -15
  69. package/lib/src/effects/exports.ts +2 -1
  70. package/lib/src/index.ts +27 -29
  71. package/lib/src/typings.ts +14 -14
  72. package/lib/src/utilities/debugger.ts +6 -6
  73. package/lib/src/utilities/helpers.ts +39 -15
  74. package/lib/src/utilities/web-assembly.ts +8 -0
  75. package/package.json +1 -1
  76. package/lib/dist/effects/classes/Filter.d.ts +0 -3
  77. package/lib/dist/effects/classes/Filter.d.ts.map +0 -1
  78. package/lib/dist/effects/classes/Filter.js +0 -2
  79. package/lib/dist/effects/classes/SoftClip.d.ts +0 -10
  80. package/lib/dist/effects/classes/SoftClip.d.ts.map +0 -1
  81. package/lib/dist/effects/classes/SoftClip.js +0 -39
  82. package/lib/dist/test/linkable-channels.d.ts +0 -2
  83. package/lib/dist/test/linkable-channels.d.ts.map +0 -1
  84. package/lib/dist/test/linkable-channels.js +0 -16
  85. 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 Send(channel: Channel) {
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 SetLabel(label: string) {
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
- private sends: Channel[] = [];
21
- private audioClipPlayer: AudioClipPlayer | null = null;
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) return (channel as Master).AttachChannel(this);
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) return Debug.Error("Cannot not link AudioClip to this channel because this channel's AudioClipPlayer is undefined.");
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 = options.pathToWasm;
50
- this.pathToWorklet = options.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 parentialContext: AudioContext | null = null;
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(parentialContext: AudioContext): Promise<void>;
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.gainNode)
38
- channel.output.connect(this.gainNode);
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.gainNode)
50
- channel.output.disconnect(this.gainNode);
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 === _channel.id) {
134
+ if (channel.id !== _channel.id) return;
56
135
 
57
- self.channels.splice(index, 1);
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
+ }