@fluex/fluexgl-dsp 0.4.7 → 0.4.8

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 (95) hide show
  1. package/README.md +7 -7
  2. package/lib/dist/console-codes.d.ts +2 -1
  3. package/lib/dist/console-codes.d.ts.map +1 -1
  4. package/lib/dist/console-codes.js +1 -0
  5. package/lib/dist/core/classes/AudioClip.d.ts +26 -17
  6. package/lib/dist/core/classes/AudioClip.d.ts.map +1 -1
  7. package/lib/dist/core/classes/AudioClip.js +124 -51
  8. package/lib/dist/core/classes/AudioClipPlayer.d.ts +8 -8
  9. package/lib/dist/core/classes/AudioClipPlayer.d.ts.map +1 -1
  10. package/lib/dist/core/classes/AudioClipPlayer.js +22 -24
  11. package/lib/dist/core/classes/AudioDevice.d.ts +46 -5
  12. package/lib/dist/core/classes/AudioDevice.d.ts.map +1 -1
  13. package/lib/dist/core/classes/AudioDevice.js +47 -6
  14. package/lib/dist/core/classes/Channel.d.ts +20 -20
  15. package/lib/dist/core/classes/Channel.d.ts.map +1 -1
  16. package/lib/dist/core/classes/Channel.js +42 -42
  17. package/lib/dist/core/classes/DpsPipeline.d.ts +6 -6
  18. package/lib/dist/core/classes/DpsPipeline.d.ts.map +1 -1
  19. package/lib/dist/core/classes/DpsPipeline.js +18 -18
  20. package/lib/dist/core/classes/Effector.d.ts +5 -5
  21. package/lib/dist/core/classes/Effector.d.ts.map +1 -1
  22. package/lib/dist/core/classes/Effector.js +8 -8
  23. package/lib/dist/core/classes/Master.d.ts +6 -6
  24. package/lib/dist/core/classes/Master.d.ts.map +1 -1
  25. package/lib/dist/core/classes/Master.js +18 -18
  26. package/lib/dist/effects/classes/Analyser.d.ts +6 -6
  27. package/lib/dist/effects/classes/Analyser.d.ts.map +1 -1
  28. package/lib/dist/effects/classes/Analyser.js +7 -7
  29. package/lib/dist/effects/classes/Chorus.d.ts +7 -7
  30. package/lib/dist/effects/classes/Chorus.d.ts.map +1 -1
  31. package/lib/dist/effects/classes/Chorus.js +25 -25
  32. package/lib/dist/effects/classes/Distortion.d.ts +1 -1
  33. package/lib/dist/effects/classes/Distortion.d.ts.map +1 -1
  34. package/lib/dist/effects/classes/Distortion.js +1 -1
  35. package/lib/dist/effects/classes/Reverb.d.ts +32 -0
  36. package/lib/dist/effects/classes/Reverb.d.ts.map +1 -0
  37. package/lib/dist/effects/classes/Reverb.js +61 -0
  38. package/lib/dist/effects/classes/clips/HardClip.d.ts +5 -5
  39. package/lib/dist/effects/classes/clips/HardClip.d.ts.map +1 -1
  40. package/lib/dist/effects/classes/clips/HardClip.js +14 -14
  41. package/lib/dist/effects/classes/clips/SoftClip.d.ts +4 -4
  42. package/lib/dist/effects/classes/clips/SoftClip.d.ts.map +1 -1
  43. package/lib/dist/effects/classes/clips/SoftClip.js +13 -13
  44. package/lib/dist/effects/classes/filters/HighPassFilter.d.ts +5 -5
  45. package/lib/dist/effects/classes/filters/HighPassFilter.d.ts.map +1 -1
  46. package/lib/dist/effects/classes/filters/HighPassFilter.js +16 -16
  47. package/lib/dist/effects/classes/filters/LowPassFilter.d.ts +5 -5
  48. package/lib/dist/effects/classes/filters/LowPassFilter.d.ts.map +1 -1
  49. package/lib/dist/effects/classes/filters/LowPassFilter.js +17 -17
  50. package/lib/dist/effects/classes/filters/NotchFilter.d.ts +5 -5
  51. package/lib/dist/effects/classes/filters/NotchFilter.d.ts.map +1 -1
  52. package/lib/dist/effects/classes/filters/NotchFilter.js +16 -16
  53. package/lib/dist/effects/exports.d.ts +1 -5
  54. package/lib/dist/effects/exports.d.ts.map +1 -1
  55. package/lib/dist/effects/exports.js +1 -5
  56. package/lib/dist/index.d.ts +2 -2
  57. package/lib/dist/index.d.ts.map +1 -1
  58. package/lib/dist/index.js +2 -2
  59. package/lib/dist/typings.d.ts +11 -0
  60. package/lib/dist/typings.d.ts.map +1 -1
  61. package/lib/dist/utilities/debugger.d.ts +4 -4
  62. package/lib/dist/utilities/debugger.js +8 -8
  63. package/lib/dist/utilities/helpers.d.ts +13 -13
  64. package/lib/dist/utilities/helpers.d.ts.map +1 -1
  65. package/lib/dist/utilities/helpers.js +45 -59
  66. package/lib/dist/utilities/web-assembly.js +1 -1
  67. package/lib/src/console-codes.ts +2 -1
  68. package/lib/src/core/classes/AudioClip.ts +154 -60
  69. package/lib/src/core/classes/AudioClipPlayer.ts +22 -24
  70. package/lib/src/core/classes/AudioDevice.ts +49 -6
  71. package/lib/src/core/classes/Channel.ts +42 -42
  72. package/lib/src/core/classes/DpsPipeline.ts +19 -19
  73. package/lib/src/core/classes/Effector.ts +9 -9
  74. package/lib/src/core/classes/Master.ts +18 -18
  75. package/lib/src/effects/classes/Analyser.ts +7 -7
  76. package/lib/src/effects/classes/Chorus.ts +25 -25
  77. package/lib/src/effects/classes/Distortion.ts +1 -1
  78. package/lib/src/effects/classes/Reverb.ts +81 -0
  79. package/lib/src/effects/classes/clips/HardClip.ts +14 -14
  80. package/lib/src/effects/classes/clips/SoftClip.ts +13 -13
  81. package/lib/src/effects/classes/filters/HighPassFilter.ts +16 -16
  82. package/lib/src/effects/classes/filters/LowPassFilter.ts +17 -17
  83. package/lib/src/effects/classes/filters/NotchFilter.ts +16 -16
  84. package/lib/src/effects/exports.ts +1 -6
  85. package/lib/src/index.ts +11 -15
  86. package/lib/src/typings.ts +13 -0
  87. package/lib/src/utilities/debugger.ts +4 -4
  88. package/lib/src/utilities/helpers.ts +47 -60
  89. package/lib/src/utilities/web-assembly.ts +1 -1
  90. package/package.json +3 -2
  91. package/lib/src/effects/classes/reverbs/ChamberReverb.ts +0 -1
  92. package/lib/src/effects/classes/reverbs/ConvolverReverb.ts +0 -1
  93. package/lib/src/effects/classes/reverbs/GenericReverb.ts +0 -1
  94. package/lib/src/effects/classes/reverbs/HallReverb.ts +0 -1
  95. package/lib/src/effects/classes/reverbs/RoomReverb.ts +0 -1
@@ -12,29 +12,29 @@ import { ErrorCodes, WarningCodes } from "../console-codes";
12
12
  * FluexGL DSP cannot be used without calling this function first.
13
13
  * @returns
14
14
  */
15
- export async function InitializeDspPipeline(options) {
16
- Debug.Log("Attempting to initialize DSP pipeline...");
15
+ export async function initializeDspPipeline(options) {
16
+ Debug.log("Attempting to initialize DSP pipeline...");
17
17
  const start = Date.now();
18
18
  let initialized = true;
19
19
  try {
20
20
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
21
- Debug.Log(`Found ${stream.getTracks().length} media stream tracks.`);
21
+ Debug.log(`Found ${stream.getTracks().length} media stream tracks.`);
22
22
  stream.getTracks().forEach(function (track) {
23
23
  track.stop();
24
24
  });
25
25
  }
26
26
  catch (err) {
27
27
  initialized = false;
28
- Debug.Error("Permission to access media devices has not been granted.", [
28
+ Debug.error("Permission to access media devices has not been granted.", [
29
29
  "Make sure the user has granted FluentGL permission to access media devices."
30
30
  ], ErrorCodes.NO_CONTEXT_PERMISSION);
31
31
  }
32
32
  await LoadWebAssemblyModule(options.pathToWasm);
33
33
  const workletFileRequest = await fetch(options.pathToWorklet);
34
34
  const textContent = await workletFileRequest.text();
35
- const blobUrl = ConstructProcessorWorklet(textContent);
35
+ const blobUrl = constructProcessorWorklet(textContent);
36
36
  const end = Date.now(), difference = end - start;
37
- Debug.Success(`Succesfully initialized DSP pipeline within ${difference}ms.`);
37
+ Debug.success(`Succesfully initialized DSP pipeline within ${difference}ms.`);
38
38
  return {
39
39
  success: true,
40
40
  workletBlobUrl: blobUrl
@@ -44,7 +44,7 @@ export async function InitializeDspPipeline(options) {
44
44
  * Resolves a list of available audio output devices.
45
45
  * @returns
46
46
  */
47
- export async function ResolveAudioOutputDevices() {
47
+ export async function resolveAudioOutputDevices() {
48
48
  const devices = await navigator.mediaDevices.enumerateDevices();
49
49
  const audioDevices = [];
50
50
  for (let device of devices)
@@ -55,7 +55,7 @@ export async function ResolveAudioOutputDevices() {
55
55
  * Resolves a list of available audio input devices.
56
56
  * @returns
57
57
  */
58
- export async function ResolveAudioInputDevices() {
58
+ export async function resolveAudioInputDevices() {
59
59
  const devices = await navigator.mediaDevices.enumerateDevices();
60
60
  const audioDevices = [];
61
61
  for (let device of devices)
@@ -66,36 +66,36 @@ export async function ResolveAudioInputDevices() {
66
66
  * Resolves the default audio output device.
67
67
  * @returns
68
68
  */
69
- export async function ResolveDefaultAudioOutputDevice(init) {
70
- Debug.Log("Attempting to resolve default audio output device...");
69
+ export async function resolveDefaultAudioOutputDevice(init) {
70
+ Debug.log("Attempting to resolve default audio output device...");
71
71
  const audioDeviceInfos = [];
72
72
  const devices = await navigator.mediaDevices.enumerateDevices();
73
73
  for (let device of devices)
74
74
  (device.kind === "audiooutput" && device.deviceId == "default")
75
75
  && audioDeviceInfos.push(device);
76
- devices.length === 0 && Debug.Warn("No default audio device found.", [], WarningCodes.NO_DEFAULT_AUDIO_DEVICE_FOUND);
76
+ devices.length === 0 && Debug.warn("No default audio device found.", [], WarningCodes.NO_DEFAULT_AUDIO_DEVICE_FOUND);
77
77
  const defaultAudioDevice = devices.length === 0 ? null : new AudioDevice(audioDeviceInfos[0]);
78
78
  if (!defaultAudioDevice)
79
79
  return null;
80
- await LoadWorkletOnAudioDevice(defaultAudioDevice, init.workletBlobUrl);
80
+ await loadWorkletOnAudioDevice(defaultAudioDevice, init.workletBlobUrl);
81
81
  return defaultAudioDevice;
82
82
  }
83
83
  /**
84
84
  * Resolves the default audio input device.
85
85
  * @returns
86
86
  */
87
- export async function ResolveDefaultAudioInputDevice(init) {
88
- Debug.Log("Attempting to resolve default audio output device...");
87
+ export async function resolveDefaultAudioInputDevice(init) {
88
+ Debug.log("Attempting to resolve default audio output device...");
89
89
  const audioDeviceInfos = [];
90
90
  const devices = await navigator.mediaDevices.enumerateDevices();
91
91
  for (let device of devices)
92
92
  (device.kind === "audioinput" && device.deviceId == "default")
93
93
  && audioDeviceInfos.push(device);
94
- devices.length === 0 && Debug.Warn("No default audio device found.", [], WarningCodes.NO_DEFAULT_AUDIO_DEVICE_FOUND);
94
+ devices.length === 0 && Debug.warn("No default audio device found.", [], WarningCodes.NO_DEFAULT_AUDIO_DEVICE_FOUND);
95
95
  const defaultAudioDevice = devices.length === 0 ? null : new AudioDevice(audioDeviceInfos[0]);
96
96
  if (!defaultAudioDevice)
97
97
  return null;
98
- await LoadWorkletOnAudioDevice(defaultAudioDevice, init.workletBlobUrl);
98
+ await loadWorkletOnAudioDevice(defaultAudioDevice, init.workletBlobUrl);
99
99
  return defaultAudioDevice;
100
100
  }
101
101
  /**
@@ -104,19 +104,19 @@ export async function ResolveDefaultAudioInputDevice(init) {
104
104
  * @param options
105
105
  * @returns
106
106
  */
107
- export async function LoadAudioSource(path, options = { allowForeignFileTypes: false }) {
107
+ export async function loadAudioSource(path, options = { allowForeignFileTypes: false }) {
108
108
  const extension = mime.getType(path);
109
109
  if (!extension && !options.allowForeignFileTypes) {
110
- Debug.Error("The file type of the specified file could not be identified.", [
110
+ Debug.error("The file type of the specified file could not be identified.", [
111
111
  `Set allowForeignFileTypes to true in the properties to allow foreign or unknown file types.`
112
112
  ], ErrorCodes.NO_FILE_TYPE_FOUND);
113
113
  return null;
114
114
  }
115
115
  if (!SUPPORTED_FILE_TYPES.includes(extension))
116
- Debug.Warn("The file type of the specified file is unknown and possibly unknown, but will be used anyways.");
116
+ Debug.warn("The file type of the specified file is unknown and possibly unknown, but will be used anyways.");
117
117
  const file = await fetch(path, { method: "get" });
118
118
  if (file.status !== 200) {
119
- Debug.Error("The specified file could not be loaded.", [
119
+ Debug.error("The specified file could not be loaded.", [
120
120
  `Received status code: ${file.status}.`
121
121
  ], ErrorCodes.PATH_TO_FILE_NOT_FOUND);
122
122
  return null;
@@ -133,7 +133,7 @@ export async function LoadAudioSource(path, options = { allowForeignFileTypes: f
133
133
  /**
134
134
  * Loads an audio file from a blob.
135
135
  */
136
- export async function LoadAudioSourceFromBlob(blob) {
136
+ export async function loadAudioSourceFromBlob(blob) {
137
137
  const tempContext = new AudioContext(), arrayBuffer = await blob.arrayBuffer();
138
138
  const audioBuffer = await tempContext.decodeAudioData(arrayBuffer);
139
139
  return {
@@ -142,27 +142,27 @@ export async function LoadAudioSourceFromBlob(blob) {
142
142
  audioBuffer, arrayBuffer
143
143
  };
144
144
  }
145
- export function ConstructProcessorWorklet(code) {
145
+ export function constructProcessorWorklet(code) {
146
146
  const blob = new Blob([code], {
147
147
  type: "application/javascript"
148
148
  });
149
149
  return URL.createObjectURL(blob);
150
150
  }
151
- export async function LoadWorkletOnAudioDevice(audioDevice, workletBlobUrl) {
152
- Debug.Log("Loading worklet modules on master channel...", [`Channel ID: ${audioDevice.id}`]);
151
+ export async function loadWorkletOnAudioDevice(audioDevice, workletBlobUrl) {
152
+ Debug.log("Loading worklet modules on master channel...", [`Channel ID: ${audioDevice.id}`]);
153
153
  const context = audioDevice.context, start = Date.now();
154
154
  await context.audioWorklet.addModule(workletBlobUrl);
155
155
  const end = Date.now(), difference = end - start;
156
- Debug.Success("Succesfully loaded audio processor worklets into master channel.", [
156
+ Debug.success("Succesfully loaded audio processor worklets into master channel.", [
157
157
  `Executed in ${difference}ms.`,
158
158
  ]);
159
159
  return true;
160
160
  }
161
- export function SendMessageToWorklet(node, commandId, data) {
161
+ export function sendMessageToWorklet(node, commandId, data) {
162
162
  if (!node)
163
163
  return false;
164
164
  if (typeof data === "number" && !Number.isFinite(data)) {
165
- Debug.Warn("Refusing to send non-finite numeric value to AudioWorkletNode.", [
165
+ Debug.warn("Refusing to send non-finite numeric value to AudioWorkletNode.", [
166
166
  `Command: ${String(commandId)}`,
167
167
  `Value: ${String(data)}`
168
168
  ]);
@@ -171,39 +171,25 @@ export function SendMessageToWorklet(node, commandId, data) {
171
171
  node.port.postMessage({ commandId, data });
172
172
  return true;
173
173
  }
174
- export function IsFiniteNumber(value) {
174
+ export function isFiniteNumber(value) {
175
175
  return typeof value === "number" && Number.isFinite(value);
176
176
  }
177
- export function CoerceFiniteNumber(value, fallback) {
178
- return IsFiniteNumber(value) ? value : fallback;
177
+ export function coerceFiniteNumber(value, fallback) {
178
+ return isFiniteNumber(value) ? value : fallback;
179
179
  }
180
- export function CreateAudioWorkletNode(context, name, data) {
181
- if (!compiledWebAssemblyModule) {
182
- Debug.Error("Failed to create audio worklet node, because the web assembly module has not been compiled properly.", [
183
- `Request audio worklet node: ${name}.`
184
- ], ErrorCodes.WORKLET_NO_WASM_MODULE_INSTANTIATED);
185
- return null;
186
- }
187
- try {
188
- return new AudioWorkletNode(context, name, {
189
- numberOfInputs: 1,
190
- numberOfOutputs: 1,
191
- outputChannelCount: [2],
192
- parameterData: {
193
- ...data,
194
- sampleRate: context.sampleRate
195
- },
196
- processorOptions: {
197
- module: compiledWebAssemblyModule
198
- }
199
- });
200
- }
201
- catch (err) {
202
- Debug.Error("Failed to create audio worklet node.", [
203
- `Request audio worklet node: ${name}.`,
204
- "Make sure you called InitializeDspPipeline/InitializeDpsPipeline and loaded the worklet module on the same AudioContext.",
205
- String(err)
206
- ]);
207
- return null;
208
- }
180
+ export function createAudioWorkletNode(context, name, data) {
181
+ if (!compiledWebAssemblyModule)
182
+ throw new Error("Coult not create audio worklet node. WebAssembly has not been compiled yet.");
183
+ return new AudioWorkletNode(context, name, {
184
+ numberOfInputs: 1,
185
+ numberOfOutputs: 1,
186
+ outputChannelCount: [2],
187
+ parameterData: {
188
+ ...data,
189
+ sampleRate: context.sampleRate
190
+ },
191
+ processorOptions: {
192
+ module: compiledWebAssemblyModule
193
+ }
194
+ });
209
195
  }
@@ -8,7 +8,7 @@ export async function LoadWebAssemblyModule(path) {
8
8
  compiledWebAssemblyModule = module;
9
9
  resolve(module);
10
10
  }).catch(function (error) {
11
- Debug.Error("Failed to load WebAssembly module from path: " + path, [
11
+ Debug.error("Failed to load WebAssembly module from path: " + path, [
12
12
  "Make sure the path is correct and the server is serving the .wasm file with the correct MIME type (application/wasm).",
13
13
  "Error details: " + error.message
14
14
  ], ErrorCodes.WASM_COMPILATION_ERROR);
@@ -19,7 +19,8 @@ export enum ErrorCodes {
19
19
  AUDIO_CLIP_ALREADY_ATTACHED = "ERROR:FLUEXGL-DSP@0018",
20
20
  AUDIO_CLIP_NOT_FOUND = "ERROR:FLUEXGL-DSP@0019",
21
21
  AUDIO_CLIP_PLAYER_NO_CONTEXT = "ERROR:FLUEXGL-DSP@0020",
22
- PANNING_OUT_OF_RANGE = "ERROR:FLUEXGL-DSP@0021"
22
+ PANNING_OUT_OF_RANGE = "ERROR:FLUEXGL-DSP@0021",
23
+ PITCH_OUT_OF_RANGE = "ERROR:FLUEXGL-DSP@0022"
23
24
  }
24
25
 
25
26
  export enum WarningCodes {
@@ -1,7 +1,7 @@
1
1
  import { v4 } from "uuid";
2
2
  import { format } from "date-fns";
3
3
 
4
- import { AudioClipEventMap, AudioClipEvents, AudioSourceData } from "../../typings";
4
+ import { AudioClipEventMap, AudioClipEvents, AudioClipOnInitializeEvent, AudioSourceData } from "../../typings";
5
5
  import { Debug } from "../../utilities/debugger";
6
6
  import { Channel } from "./Channel";
7
7
  import { AudioClipPlayer } from "./AudioClipPlayer";
@@ -9,7 +9,9 @@ import { Master } from "./Master";
9
9
  import { DSP } from "../../index";
10
10
  import { ErrorCodes } from "../../console-codes";
11
11
 
12
- type ProgressPayload = Parameters<AudioClipEventMap["progress"]>[0];
12
+ type OnProgressCallbackFunction = AudioClipEventMap["progress"];
13
+ type OnInitializeCallbackFunction = AudioClipEventMap["initialize"];
14
+ type OnPlayCallbackFunction = AudioClipEventMap["play"];
13
15
 
14
16
  export class AudioClip {
15
17
 
@@ -21,6 +23,10 @@ export class AudioClip {
21
23
  public startTime: number = 0;
22
24
  public offsetAtStart: number = 0;
23
25
  public playbackRate: number = 1;
26
+ public pitch: number = 0;
27
+
28
+ public minPitchSemitones: number = -24;
29
+ public maxPitchSemitones: number = 24;
24
30
 
25
31
  public progressUpdateSpeed: number = 20;
26
32
 
@@ -32,11 +38,14 @@ export class AudioClip {
32
38
 
33
39
  private audioBufferSourceNodes: AudioBufferSourceNode[] = [];
34
40
  private maxAudioBufferSourceNodes: number = 1;
41
+ private audioClipPlayers: AudioClipPlayer[] = [];
35
42
 
36
43
  private progressInterval: number | null = 0;
37
44
 
38
45
  private events: AudioClipEvents = {
39
- "progress": []
46
+ "progress": [],
47
+ "initialize": [],
48
+ "play": []
40
49
  }
41
50
 
42
51
  constructor(public data: AudioSourceData) { }
@@ -66,54 +75,98 @@ export class AudioClip {
66
75
  node?.disconnect();
67
76
  }
68
77
 
78
+ private getOutputNodes(): AudioNode[] {
79
+
80
+ const nodes: AudioNode[] = [];
81
+
82
+ for (const player of this.audioClipPlayers) {
83
+ if (player.outputGainNode && !nodes.includes(player.outputGainNode))
84
+ nodes.push(player.outputGainNode);
85
+ }
86
+
87
+ return nodes;
88
+ }
89
+
69
90
  private rebuildNodeChain(): boolean {
70
91
 
71
- if (!this.context || !this.gainNode || !this.stereoPannerNode || !this.audioClipPlayer || !this.audioClipPlayer.outputGainNode) {
72
- Debug.Error("Failed to rebuild node chain, because some of the core AudioNodes are missing.");
92
+ if (!this.context || !this.gainNode || !this.stereoPannerNode) {
93
+ Debug.error("Failed to rebuild node chain, because some of the core AudioNodes are missing.");
73
94
  return false;
74
95
  }
75
96
 
76
- const destination = this.audioClipPlayer.outputGainNode;
97
+ const destinations = this.getOutputNodes();
98
+
99
+ if (destinations.length === 0) {
100
+ Debug.error("Failed to rebuild node chain, because no output destinations are available.");
101
+ return false;
102
+ }
77
103
 
78
104
  this.safeDisconnect(this.gainNode);
79
105
  this.safeDisconnect(this.stereoPannerNode);
80
106
 
81
107
  this.gainNode.connect(this.stereoPannerNode);
82
- this.stereoPannerNode.connect(destination);
108
+ for (const destination of destinations) {
109
+ this.stereoPannerNode.connect(destination);
110
+ }
83
111
 
84
112
  this.connectSourcesTo(this.gainNode);
85
113
  return true;
86
114
  }
87
115
 
88
- public Initialize(audioClipPlayer: AudioClipPlayer) {
116
+ public initialize(audioClipPlayer: AudioClipPlayer) {
117
+
118
+ const startTimestamp: number = Date.now(),
119
+ self: AudioClip = this;
89
120
 
90
- if (!audioClipPlayer.context) return Debug.Error("Could not initialize AudioClip, because the AudioClipPlayer somehow has no audio context.", [
121
+ if (!audioClipPlayer.context) return Debug.error("Could not initialize AudioClip, because the AudioClipPlayer somehow has no audio context.", [
91
122
  `AudioClipPlayer id: ${audioClipPlayer.id}`,
92
123
  `AudioClip id: ${this.id}`
93
124
  ], ErrorCodes.AUDIO_CLIP_PLAYER_NO_CONTEXT);
94
125
 
126
+ if (this.context && this.context !== audioClipPlayer.context) {
127
+ return Debug.error("Could not initialize AudioClip, because the AudioClipPlayer does not share the same AudioContext.", [
128
+ `AudioClipPlayer id: ${audioClipPlayer.id}`,
129
+ `AudioClip id: ${this.id}`
130
+ ], ErrorCodes.AUDIO_CLIP_PLAYER_NO_CONTEXT);
131
+ }
132
+
95
133
  this.audioClipPlayer = audioClipPlayer;
96
134
  this.context = audioClipPlayer.context;
97
135
 
98
- this.gainNode = new GainNode(this.context);
99
- this.stereoPannerNode = new StereoPannerNode(this.context);
136
+ if (!this.gainNode) this.gainNode = new GainNode(this.context);
137
+ if (!this.stereoPannerNode) this.stereoPannerNode = new StereoPannerNode(this.context);
138
+
139
+ if (!this.audioClipPlayers.includes(audioClipPlayer))
140
+ this.audioClipPlayers.push(audioClipPlayer);
141
+
142
+ const endTimestamp: number = Date.now(),
143
+ difference: number = endTimestamp - startTimestamp;
144
+
145
+ this.events.initialize.forEach(function(cb: OnInitializeCallbackFunction) {
146
+ cb({
147
+ durationOfInitialization: difference,
148
+ context: self.context
149
+ });
150
+ });
151
+
152
+ this.rebuildNodeChain();
100
153
  }
101
154
 
102
- public Play(timestamp?: number, offset?: number) {
155
+ public play(timestamp?: number, offset?: number) {
103
156
 
104
- if (!this.context) return Debug.Error("Could not play audio clip, because context (AudioContext) is undefined.", [
105
- "Make sure to attach this AudioClip to an AudioClipPlayer instance, before calling .Play on this AudioClip."
157
+ if (!this.context) return Debug.error("Could not play audio clip, because context (AudioContext) is undefined.", [
158
+ "Make sure to attach this AudioClip to an AudioClipPlayer instance, before calling .play on this AudioClip."
106
159
  ]);
107
160
 
108
- const context = this.context;
109
- const self = this;
161
+ const context = this.context,
162
+ self = this;
110
163
 
111
164
  if (this.audioBufferSourceNodes.length > this.maxAudioBufferSourceNodes - 1) return null;
112
165
 
113
166
  const bufferSource: AudioBufferSourceNode | null = this.createBufferSource();
114
167
 
115
168
  if (!bufferSource) {
116
- Debug.Error("Something went wrong.");
169
+ Debug.error("Something went wrong.");
117
170
  return null;
118
171
  }
119
172
 
@@ -134,7 +187,7 @@ export class AudioClip {
134
187
 
135
188
  const formattedTime = format(date, "mm:ss");
136
189
 
137
- const progressPayload: ProgressPayload = {
190
+ const progressPayload: Parameters<AudioClipEventMap["progress"]>[0] = {
138
191
  current: parseFloat(current.toFixed(2)),
139
192
  startTime: self.startTime,
140
193
  offset: self.offsetAtStart,
@@ -142,7 +195,7 @@ export class AudioClip {
142
195
  formatted: formattedTime
143
196
  }
144
197
 
145
- self.events.progress.forEach(function (cb: (event: ProgressPayload) => void) {
198
+ self.events.progress.forEach(function (cb: OnProgressCallbackFunction) {
146
199
  cb(progressPayload);
147
200
  });
148
201
  }, this.progressUpdateSpeed);
@@ -165,13 +218,21 @@ export class AudioClip {
165
218
 
166
219
  bufferSource.start(timestamp ?? this.startTime, actualOffset);
167
220
 
221
+ this.events.play.forEach(function(cb: OnPlayCallbackFunction) {
222
+ cb({
223
+ timestamp: Date.now(),
224
+ audioBufferSourceNodes: self.audioBufferSourceNodes,
225
+ context: context,
226
+ })
227
+ });
228
+
168
229
  return this;
169
230
  }
170
231
 
171
- public Seek(seconds: number) {
232
+ public seek(seconds: number) {
172
233
 
173
- if (!this.context) return Debug.Error("Could not seek because the context (AudioContext) of this AudioClip is undefined.", [
174
- `Make sure to attach this AudioClip to an AudioClipPlayer instance before calling .Seek on this AudioClip.`,
234
+ if (!this.context) return Debug.error("Could not seek because the context (AudioContext) of this AudioClip is undefined.", [
235
+ `Make sure to attach this AudioClip to an AudioClipPlayer instance before calling .seek on this AudioClip.`,
175
236
  `AudioClip id: ${this.id}`
176
237
  ]);
177
238
 
@@ -184,13 +245,13 @@ export class AudioClip {
184
245
  return this;
185
246
  }
186
247
 
187
- this.Stop();
188
- this.Play(undefined, clamped);
248
+ this.stop();
249
+ this.play(undefined, clamped);
189
250
 
190
251
  return this;
191
252
  }
192
253
 
193
- public Stop(): AudioClip | null {
254
+ public stop(): AudioClip | null {
194
255
 
195
256
  this.audioBufferSourceNodes.forEach(function (node: AudioBufferSourceNode) {
196
257
  node.stop();
@@ -208,23 +269,23 @@ export class AudioClip {
208
269
  return this;
209
270
  }
210
271
 
211
- public SetVolume(volume: number): AudioClip {
272
+ public setVolume(volume: number): AudioClip {
212
273
 
213
- if (!this.context) Debug.Error("Could not set volume because the context (AudioContext) of this AudioClip is undefined.", [
214
- "Make sure to attach this AudioClip to an AudioClipPlayer instance before calling .SetVolume() on this AudioClip.",
274
+ if (!this.context) Debug.error("Could not set volume because the context (AudioContext) of this AudioClip is undefined.", [
275
+ "Make sure to attach this AudioClip to an AudioClipPlayer instance before calling .setVolume() on this AudioClip.",
215
276
  ], ErrorCodes.AUDIO_CLIP_PLAYER_NO_CONTEXT);
216
277
 
217
278
  this.gainNode?.gain.setValueAtTime(volume, this.context?.currentTime ?? 0);
218
279
  return this;
219
280
  }
220
281
 
221
- public SetPanLevel(panLevel: number): AudioClip {
282
+ public setPanLevel(panLevel: number): AudioClip {
222
283
 
223
- if (!this.context) Debug.Error("Could not set volume because the context (AudioContext) of this AudioClip is undefined.", [
224
- "Make sure to attach this AudioClip to an AudioClipPlayer instance before calling .SetPanLevel() on this AudioClip.",
284
+ if (!this.context) Debug.error("Could not set volume because the context (AudioContext) of this AudioClip is undefined.", [
285
+ "Make sure to attach this AudioClip to an AudioClipPlayer instance before calling .setPanLevel() on this AudioClip.",
225
286
  ], ErrorCodes.AUDIO_CLIP_PLAYER_NO_CONTEXT);
226
287
 
227
- if (panLevel < -1 || panLevel > 1) Debug.Error("Could not set the pan level because it is not between -1 and 1.", [
288
+ if (panLevel < -1 || panLevel > 1) Debug.error("Could not set the pan level because it is not between -1 and 1.", [
228
289
  `Given value: ${panLevel}.`,
229
290
  `Accepts a value between -1 and 1. Can be a floating number.`
230
291
  ], ErrorCodes.PANNING_OUT_OF_RANGE);
@@ -233,7 +294,7 @@ export class AudioClip {
233
294
  return this;
234
295
  }
235
296
 
236
- public Loop(loop?: boolean): AudioClip {
297
+ public setLoop(loop?: boolean): AudioClip {
237
298
 
238
299
  this.audioBufferSourceNodes.forEach(function (node: AudioBufferSourceNode) {
239
300
  node.loop = loop ?? true;
@@ -243,10 +304,10 @@ export class AudioClip {
243
304
  return this;
244
305
  }
245
306
 
246
- public SetMaxAudioBufferSourceNodes(value: number): AudioClip {
307
+ public setMaxAudioBufferSourceNodes(value: number): AudioClip {
247
308
 
248
309
  if (!DSP.overrideMaxAudioBufferNodes) {
249
- Debug.Warn("Cannot set maxAudioBufferSourceNodes because the option to override the current maximum value is disabled.", [
310
+ Debug.warn("Cannot set maxAudioBufferSourceNodes because the option to override the current maximum value is disabled.", [
250
311
  "Set overrideMaxAudioBufferNodes on DSP to true, in order to override the maximum audio buffer source nodes."
251
312
  ]);
252
313
  return this;
@@ -256,7 +317,7 @@ export class AudioClip {
256
317
  return this;
257
318
  }
258
319
 
259
- public DisconnectAllAudioBufferSourceNodes(): boolean {
320
+ public disconnectAllAudioBufferSourceNodes(): boolean {
260
321
 
261
322
  if (!this.context) return false;
262
323
 
@@ -270,25 +331,25 @@ export class AudioClip {
270
331
  return true;
271
332
  }
272
333
 
273
- public AddEventListener<K extends keyof AudioClipEventMap>(event: K, cb: AudioClipEventMap[K]): () => void {
334
+ public addEventListener<K extends keyof AudioClipEventMap>(event: K, cb: AudioClipEventMap[K]): () => void {
274
335
 
275
336
  this.events[event].push(cb);
276
- return () => this.RemoveEventListener(event, cb);
337
+ return () => this.removeEventListener(event, cb);
277
338
  }
278
339
 
279
- public Once<K extends keyof AudioClipEventMap>(event: K, cb: AudioClipEventMap[K]): () => void {
340
+ public once<K extends keyof AudioClipEventMap>(event: K, cb: AudioClipEventMap[K]): () => void {
280
341
 
281
342
  const wrapper = ((...args: unknown[]) => {
282
343
 
283
344
  // @ts-ignore
284
345
  cb(...args);
285
- this.RemoveEventListener(event, wrapper as unknown as AudioClipEventMap[K]);
346
+ this.removeEventListener(event, wrapper as unknown as AudioClipEventMap[K]);
286
347
  }) as unknown as AudioClipEventMap[K];
287
348
 
288
- return this.AddEventListener(event, wrapper);
349
+ return this.addEventListener(event, wrapper);
289
350
  }
290
351
 
291
- public RemoveEventListener<K extends keyof AudioClipEventMap>(event: K, cb: AudioClipEventMap[K]): AudioClip {
352
+ public removeEventListener<K extends keyof AudioClipEventMap>(event: K, cb: AudioClipEventMap[K]): AudioClip {
292
353
 
293
354
  const arr = this.events[event];
294
355
 
@@ -301,7 +362,7 @@ export class AudioClip {
301
362
  return this;
302
363
  }
303
364
 
304
- public ClearEventListeners(event?: keyof AudioClipEventMap): AudioClip {
365
+ public clearEventListeners(event?: keyof AudioClipEventMap): AudioClip {
305
366
 
306
367
  if (event) {
307
368
  this.events[event].length = 0;
@@ -312,47 +373,82 @@ export class AudioClip {
312
373
  return this;
313
374
  }
314
375
 
315
- public GetChannelData(channel: number = 0): Float32Array {
376
+ public getChannelData(channel: number = 0): Float32Array {
316
377
  return this.data.audioBuffer.getChannelData(channel);
317
378
  }
318
379
 
319
- public Send(channel: Channel | Master) {
380
+ public send(channel: Channel | Master) {
320
381
 
321
- if (!channel.audioClipPlayer) return Debug.Error("Could not send AudioClip signal to channel, because the channel's AudioClipPlayer is undefined.", [
382
+ if (!channel.audioClipPlayer) return Debug.error("Could not send AudioClip signal to channel, because the channel's AudioClipPlayer is undefined.", [
322
383
  "Make sure to initialize the Channel.",
323
384
  `Channel id: ${channel.id}.`,
324
385
  `AudioClip id: ${this.id}.`
325
386
  ]);
326
387
 
327
- channel.audioClipPlayer.AttachAudioClip(this);
388
+ channel.audioClipPlayer.attachAudioClip(this);
328
389
  }
329
390
 
330
- public Unsend(channel: Channel | Master) {
391
+ public unsend(channel: Channel | Master) {
331
392
 
332
- if (!channel.audioClipPlayer) return Debug.Error("Could not unsend AudioClip signal to channel, because the channel's AudioClipPlayer is undefined.", [
393
+ if (!channel.audioClipPlayer) return Debug.error("Could not unsend AudioClip signal to channel, because the channel's AudioClipPlayer is undefined.", [
333
394
  "Make sure to initialize the Channel.",
334
395
  `Channel id: ${channel.id}.`,
335
396
  `AudioClip id: ${this.id}.`
336
397
  ]);
337
398
 
338
- channel.audioClipPlayer.DetachAudioClip(this);
399
+ channel.audioClipPlayer.detachAudioClip(this);
339
400
  }
340
401
 
341
- public SetPlaybackRateInSemitones(semitones: number): AudioClip {
402
+ public detachFromAudioClipPlayer(audioClipPlayer: AudioClipPlayer): void {
342
403
 
343
- if(!this.context)
344
- throw new Error("Could not set playback rate because the AudioContext is not defined.");
404
+ const idx = this.audioClipPlayers.indexOf(audioClipPlayer);
345
405
 
346
- const calculatedPlaybackRate: number = Math.pow(2, semitones / 12);
347
-
348
- for(const buffer of this.audioBufferSourceNodes) {
349
- buffer.playbackRate.setValueAtTime(calculatedPlaybackRate, this.context?.currentTime);
406
+ if (idx === -1) return;
407
+
408
+ this.audioClipPlayers.splice(idx, 1);
409
+
410
+ if (this.audioClipPlayer === audioClipPlayer) {
411
+ this.audioClipPlayer = this.audioClipPlayers[0] ?? null;
412
+ }
413
+
414
+ if (this.audioClipPlayers.length === 0) {
415
+ this.stop();
416
+ return;
350
417
  }
351
418
 
419
+ this.rebuildNodeChain();
420
+ }
421
+
422
+ public setPitch(semitones: number): AudioClip {
423
+
424
+ if (!this.context) Debug.error("Could not set pitch because the context (AudioContext) of this AudioClip is undefined.", [
425
+ "Make sure to attach this AudioClip to an AudioClipPlayer instance before calling .setPitch() on this AudioClip.",
426
+ ], ErrorCodes.AUDIO_CLIP_PLAYER_NO_CONTEXT);
427
+
428
+ if (semitones < this.minPitchSemitones || semitones > this.maxPitchSemitones) Debug.error("Could not set the pitch because it is not within the allowed semitone range.", [
429
+ `Given value: ${semitones}.`,
430
+ `Accepts a value between ${this.minPitchSemitones} and ${this.maxPitchSemitones} semitones.`
431
+ ], ErrorCodes.PITCH_OUT_OF_RANGE);
432
+
433
+ const calculatedPlaybackRate: number = Math.pow(2, semitones / 12);
434
+
435
+ for (const buffer of this.audioBufferSourceNodes)
436
+ buffer.playbackRate.setValueAtTime(calculatedPlaybackRate, this.context?.currentTime ?? 0);
437
+
438
+ this.pitch = semitones;
352
439
  this.playbackRate = calculatedPlaybackRate;
353
440
  return this;
354
441
  }
355
442
 
443
+ public resetPitch(): AudioClip {
444
+ return this.setPitch(0);
445
+ }
446
+
447
+ /** @deprecated Use {@link setPitch} instead. */
448
+ public setPlaybackRateInSemitones(semitones: number): AudioClip {
449
+ return this.setPitch(semitones);
450
+ }
451
+
356
452
  public get currentPlaybackTime(): number {
357
453
  return (!this.isPlaying || !this.context)
358
454
  ? 0
@@ -372,9 +468,7 @@ export class AudioClip {
372
468
  }
373
469
 
374
470
  public get formattedDuration(): string {
375
-
376
471
  const date = new Date(this.duration * 1000);
377
-
378
472
  return format(date, "mm:ss");
379
473
  }
380
474
 
@@ -389,4 +483,4 @@ export class AudioClip {
389
483
  public get byteLength(): number {
390
484
  return this.data.arrayBuffer.byteLength;
391
485
  }
392
- }
486
+ }