@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.
- package/README.md +7 -7
- package/lib/dist/console-codes.d.ts +2 -1
- package/lib/dist/console-codes.d.ts.map +1 -1
- package/lib/dist/console-codes.js +1 -0
- package/lib/dist/core/classes/AudioClip.d.ts +26 -17
- package/lib/dist/core/classes/AudioClip.d.ts.map +1 -1
- package/lib/dist/core/classes/AudioClip.js +124 -51
- package/lib/dist/core/classes/AudioClipPlayer.d.ts +8 -8
- package/lib/dist/core/classes/AudioClipPlayer.d.ts.map +1 -1
- package/lib/dist/core/classes/AudioClipPlayer.js +22 -24
- package/lib/dist/core/classes/AudioDevice.d.ts +46 -5
- package/lib/dist/core/classes/AudioDevice.d.ts.map +1 -1
- package/lib/dist/core/classes/AudioDevice.js +47 -6
- package/lib/dist/core/classes/Channel.d.ts +20 -20
- package/lib/dist/core/classes/Channel.d.ts.map +1 -1
- package/lib/dist/core/classes/Channel.js +42 -42
- package/lib/dist/core/classes/DpsPipeline.d.ts +6 -6
- package/lib/dist/core/classes/DpsPipeline.d.ts.map +1 -1
- package/lib/dist/core/classes/DpsPipeline.js +18 -18
- package/lib/dist/core/classes/Effector.d.ts +5 -5
- package/lib/dist/core/classes/Effector.d.ts.map +1 -1
- package/lib/dist/core/classes/Effector.js +8 -8
- package/lib/dist/core/classes/Master.d.ts +6 -6
- package/lib/dist/core/classes/Master.d.ts.map +1 -1
- package/lib/dist/core/classes/Master.js +18 -18
- package/lib/dist/effects/classes/Analyser.d.ts +6 -6
- package/lib/dist/effects/classes/Analyser.d.ts.map +1 -1
- package/lib/dist/effects/classes/Analyser.js +7 -7
- package/lib/dist/effects/classes/Chorus.d.ts +7 -7
- package/lib/dist/effects/classes/Chorus.d.ts.map +1 -1
- package/lib/dist/effects/classes/Chorus.js +25 -25
- package/lib/dist/effects/classes/Distortion.d.ts +1 -1
- package/lib/dist/effects/classes/Distortion.d.ts.map +1 -1
- package/lib/dist/effects/classes/Distortion.js +1 -1
- package/lib/dist/effects/classes/Reverb.d.ts +32 -0
- package/lib/dist/effects/classes/Reverb.d.ts.map +1 -0
- package/lib/dist/effects/classes/Reverb.js +61 -0
- package/lib/dist/effects/classes/clips/HardClip.d.ts +5 -5
- package/lib/dist/effects/classes/clips/HardClip.d.ts.map +1 -1
- package/lib/dist/effects/classes/clips/HardClip.js +14 -14
- package/lib/dist/effects/classes/clips/SoftClip.d.ts +4 -4
- package/lib/dist/effects/classes/clips/SoftClip.d.ts.map +1 -1
- package/lib/dist/effects/classes/clips/SoftClip.js +13 -13
- package/lib/dist/effects/classes/filters/HighPassFilter.d.ts +5 -5
- package/lib/dist/effects/classes/filters/HighPassFilter.d.ts.map +1 -1
- package/lib/dist/effects/classes/filters/HighPassFilter.js +16 -16
- package/lib/dist/effects/classes/filters/LowPassFilter.d.ts +5 -5
- package/lib/dist/effects/classes/filters/LowPassFilter.d.ts.map +1 -1
- package/lib/dist/effects/classes/filters/LowPassFilter.js +17 -17
- package/lib/dist/effects/classes/filters/NotchFilter.d.ts +5 -5
- package/lib/dist/effects/classes/filters/NotchFilter.d.ts.map +1 -1
- package/lib/dist/effects/classes/filters/NotchFilter.js +16 -16
- package/lib/dist/effects/exports.d.ts +1 -5
- package/lib/dist/effects/exports.d.ts.map +1 -1
- package/lib/dist/effects/exports.js +1 -5
- package/lib/dist/index.d.ts +2 -2
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +2 -2
- package/lib/dist/typings.d.ts +11 -0
- package/lib/dist/typings.d.ts.map +1 -1
- package/lib/dist/utilities/debugger.d.ts +4 -4
- package/lib/dist/utilities/debugger.js +8 -8
- package/lib/dist/utilities/helpers.d.ts +13 -13
- package/lib/dist/utilities/helpers.d.ts.map +1 -1
- package/lib/dist/utilities/helpers.js +45 -59
- package/lib/dist/utilities/web-assembly.js +1 -1
- package/lib/src/console-codes.ts +2 -1
- package/lib/src/core/classes/AudioClip.ts +154 -60
- package/lib/src/core/classes/AudioClipPlayer.ts +22 -24
- package/lib/src/core/classes/AudioDevice.ts +49 -6
- package/lib/src/core/classes/Channel.ts +42 -42
- package/lib/src/core/classes/DpsPipeline.ts +19 -19
- package/lib/src/core/classes/Effector.ts +9 -9
- package/lib/src/core/classes/Master.ts +18 -18
- package/lib/src/effects/classes/Analyser.ts +7 -7
- package/lib/src/effects/classes/Chorus.ts +25 -25
- package/lib/src/effects/classes/Distortion.ts +1 -1
- package/lib/src/effects/classes/Reverb.ts +81 -0
- package/lib/src/effects/classes/clips/HardClip.ts +14 -14
- package/lib/src/effects/classes/clips/SoftClip.ts +13 -13
- package/lib/src/effects/classes/filters/HighPassFilter.ts +16 -16
- package/lib/src/effects/classes/filters/LowPassFilter.ts +17 -17
- package/lib/src/effects/classes/filters/NotchFilter.ts +16 -16
- package/lib/src/effects/exports.ts +1 -6
- package/lib/src/index.ts +11 -15
- package/lib/src/typings.ts +13 -0
- package/lib/src/utilities/debugger.ts +4 -4
- package/lib/src/utilities/helpers.ts +47 -60
- package/lib/src/utilities/web-assembly.ts +1 -1
- package/package.json +3 -2
- package/lib/src/effects/classes/reverbs/ChamberReverb.ts +0 -1
- package/lib/src/effects/classes/reverbs/ConvolverReverb.ts +0 -1
- package/lib/src/effects/classes/reverbs/GenericReverb.ts +0 -1
- package/lib/src/effects/classes/reverbs/HallReverb.ts +0 -1
- 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
|
|
16
|
-
Debug.
|
|
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.
|
|
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.
|
|
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 =
|
|
35
|
+
const blobUrl = constructProcessorWorklet(textContent);
|
|
36
36
|
const end = Date.now(), difference = end - start;
|
|
37
|
-
Debug.
|
|
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
|
|
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
|
|
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
|
|
70
|
-
Debug.
|
|
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.
|
|
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
|
|
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
|
|
88
|
-
Debug.
|
|
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.
|
|
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
|
|
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
|
|
107
|
+
export async function loadAudioSource(path, options = { allowForeignFileTypes: false }) {
|
|
108
108
|
const extension = mime.getType(path);
|
|
109
109
|
if (!extension && !options.allowForeignFileTypes) {
|
|
110
|
-
Debug.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
152
|
-
Debug.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
174
|
+
export function isFiniteNumber(value) {
|
|
175
175
|
return typeof value === "number" && Number.isFinite(value);
|
|
176
176
|
}
|
|
177
|
-
export function
|
|
178
|
-
return
|
|
177
|
+
export function coerceFiniteNumber(value, fallback) {
|
|
178
|
+
return isFiniteNumber(value) ? value : fallback;
|
|
179
179
|
}
|
|
180
|
-
export function
|
|
181
|
-
if (!compiledWebAssemblyModule)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
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);
|
package/lib/src/console-codes.ts
CHANGED
|
@@ -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
|
|
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
|
|
72
|
-
Debug.
|
|
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
|
|
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
|
-
|
|
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
|
|
116
|
+
public initialize(audioClipPlayer: AudioClipPlayer) {
|
|
117
|
+
|
|
118
|
+
const startTimestamp: number = Date.now(),
|
|
119
|
+
self: AudioClip = this;
|
|
89
120
|
|
|
90
|
-
if (!audioClipPlayer.context) return Debug.
|
|
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
|
|
155
|
+
public play(timestamp?: number, offset?: number) {
|
|
103
156
|
|
|
104
|
-
if (!this.context) return Debug.
|
|
105
|
-
"Make sure to attach this AudioClip to an AudioClipPlayer instance, before calling .
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
|
232
|
+
public seek(seconds: number) {
|
|
172
233
|
|
|
173
|
-
if (!this.context) return Debug.
|
|
174
|
-
`Make sure to attach this AudioClip to an AudioClipPlayer instance before calling .
|
|
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.
|
|
188
|
-
this.
|
|
248
|
+
this.stop();
|
|
249
|
+
this.play(undefined, clamped);
|
|
189
250
|
|
|
190
251
|
return this;
|
|
191
252
|
}
|
|
192
253
|
|
|
193
|
-
public
|
|
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
|
|
272
|
+
public setVolume(volume: number): AudioClip {
|
|
212
273
|
|
|
213
|
-
if (!this.context) Debug.
|
|
214
|
-
"Make sure to attach this AudioClip to an AudioClipPlayer instance before calling .
|
|
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
|
|
282
|
+
public setPanLevel(panLevel: number): AudioClip {
|
|
222
283
|
|
|
223
|
-
if (!this.context) Debug.
|
|
224
|
-
"Make sure to attach this AudioClip to an AudioClipPlayer instance before calling .
|
|
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.
|
|
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
|
|
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
|
|
307
|
+
public setMaxAudioBufferSourceNodes(value: number): AudioClip {
|
|
247
308
|
|
|
248
309
|
if (!DSP.overrideMaxAudioBufferNodes) {
|
|
249
|
-
Debug.
|
|
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
|
|
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
|
|
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.
|
|
337
|
+
return () => this.removeEventListener(event, cb);
|
|
277
338
|
}
|
|
278
339
|
|
|
279
|
-
public
|
|
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.
|
|
346
|
+
this.removeEventListener(event, wrapper as unknown as AudioClipEventMap[K]);
|
|
286
347
|
}) as unknown as AudioClipEventMap[K];
|
|
287
348
|
|
|
288
|
-
return this.
|
|
349
|
+
return this.addEventListener(event, wrapper);
|
|
289
350
|
}
|
|
290
351
|
|
|
291
|
-
public
|
|
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
|
|
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
|
|
376
|
+
public getChannelData(channel: number = 0): Float32Array {
|
|
316
377
|
return this.data.audioBuffer.getChannelData(channel);
|
|
317
378
|
}
|
|
318
379
|
|
|
319
|
-
public
|
|
380
|
+
public send(channel: Channel | Master) {
|
|
320
381
|
|
|
321
|
-
if (!channel.audioClipPlayer) return Debug.
|
|
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.
|
|
388
|
+
channel.audioClipPlayer.attachAudioClip(this);
|
|
328
389
|
}
|
|
329
390
|
|
|
330
|
-
public
|
|
391
|
+
public unsend(channel: Channel | Master) {
|
|
331
392
|
|
|
332
|
-
if (!channel.audioClipPlayer) return Debug.
|
|
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.
|
|
399
|
+
channel.audioClipPlayer.detachAudioClip(this);
|
|
339
400
|
}
|
|
340
401
|
|
|
341
|
-
public
|
|
402
|
+
public detachFromAudioClipPlayer(audioClipPlayer: AudioClipPlayer): void {
|
|
342
403
|
|
|
343
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
}
|