@effetune/dsp 0.4.0 → 0.6.0
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 +114 -11
- package/dist/assets/effetune-dsp.meta.json +110 -37
- package/dist/assets/effetune-dsp.simd.wasm +0 -0
- package/dist/assets/effetune-dsp.wasm +0 -0
- package/dist/assets.js +23 -0
- package/dist/catalog/effects-v1.json +1041 -87
- package/dist/denormal-noise.js +15 -0
- package/dist/effect.js +1 -0
- package/dist/errors.js +21 -0
- package/dist/generated-effects.d.ts +201 -5
- package/dist/generated-effects.js +120 -3
- package/dist/generated-graph-contract.js +8 -0
- package/dist/graph-document.js +644 -0
- package/dist/graph-engine.js +493 -0
- package/dist/graph.js +467 -0
- package/dist/index.d.ts +253 -1
- package/dist/index.js +26 -0
- package/dist/internal/dsp-engine-binding.js +142 -1
- package/dist/internal/dsp-params.generated.js +438 -16
- package/dist/preset.js +29 -0
- package/dist/runtime.js +73 -5
- package/dist/schemas/chain-v1.schema.json +1109 -48
- package/dist/schemas/graph-v1.schema.json +8416 -0
- package/dist/semantics.js +89 -30
- package/dist/worklet-processor.js +8 -1
- package/dist/worklet.d.ts +1 -0
- package/dist/worklet.js +15 -0
- package/package.json +2 -1
package/dist/semantics.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { DSP_PARAM_PACKERS } from './internal/dsp-params.generated.js';
|
|
2
2
|
import { getEffectDefinition, getEffectImplementation } from './catalog.js';
|
|
3
3
|
import { Effect, validateChannel, validateParameterValue } from './effect.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
AssetError,
|
|
6
|
+
EffeTuneError,
|
|
7
|
+
EffectError,
|
|
8
|
+
ValidationError,
|
|
9
|
+
withValidationDetail
|
|
10
|
+
} from './errors.js';
|
|
5
11
|
|
|
6
12
|
const DOCUMENT_KEYS = new Set(['version', 'chain']);
|
|
7
13
|
const EFFECT_KEYS = new Set(['id', 'type', 'enabled', 'channel', 'parameters', 'assets']);
|
|
@@ -9,6 +15,7 @@ const STREAM_RECONFIGURATION_PARAMETERS = new Map([
|
|
|
9
15
|
['FIRCrossover', new Set(['bandCount', 'latencyMode', 'filterDelaySamples'])],
|
|
10
16
|
['FiveBandFIRPEQ', new Set(['latencyMode', 'filterDelaySamples'])],
|
|
11
17
|
['GroupDelayEQ', new Set(['latencyMode', 'filterDelaySamples'])],
|
|
18
|
+
['GroupDelayPEQ', new Set(['latencyMode', 'filterDelaySamples'])],
|
|
12
19
|
['IRReverb', new Set(['channelMode', 'latency', 'convolutionRate'])],
|
|
13
20
|
['RoomEQ', new Set(['latencyMode', 'filterDelaySamples'])]
|
|
14
21
|
]);
|
|
@@ -51,34 +58,47 @@ function cloneEffect(effect) {
|
|
|
51
58
|
};
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
function fromPlainEffect(value, index) {
|
|
55
|
-
if (!isRecord(value)) throw new ValidationError(
|
|
61
|
+
function fromPlainEffect(value, index, label = `Chain entry ${index}`) {
|
|
62
|
+
if (!isRecord(value)) throw new ValidationError(`${label} must be an object.`);
|
|
56
63
|
for (const key of Object.keys(value)) {
|
|
57
64
|
if (!EFFECT_KEYS.has(key)) {
|
|
58
|
-
throw new ValidationError(
|
|
65
|
+
throw new ValidationError(`${label} has an unsupported field: ${key}`);
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
if (typeof value.type !== 'string') {
|
|
62
|
-
throw new ValidationError(
|
|
69
|
+
throw new ValidationError(`${label} requires an effect type.`);
|
|
63
70
|
}
|
|
64
71
|
if (!isRecord(value.parameters)) {
|
|
65
|
-
throw new ValidationError(
|
|
72
|
+
throw new ValidationError(`${label} requires a parameters object.`);
|
|
73
|
+
}
|
|
74
|
+
let definition;
|
|
75
|
+
try {
|
|
76
|
+
definition = getEffectDefinition(value.type);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
throw withValidationDetail(error, { kind: 'type' });
|
|
66
79
|
}
|
|
67
|
-
const definition = getEffectDefinition(value.type);
|
|
68
80
|
const id = value.id;
|
|
69
81
|
if (id !== undefined &&
|
|
70
82
|
(typeof id !== 'string' || id.length < 1 || id.length > 128)) {
|
|
71
|
-
throw new ValidationError(
|
|
83
|
+
throw new ValidationError(`${label} has an invalid effect id.`);
|
|
72
84
|
}
|
|
73
85
|
const enabled = value.enabled ?? true;
|
|
74
86
|
if (typeof enabled !== 'boolean') {
|
|
75
|
-
throw new ValidationError(
|
|
87
|
+
throw new ValidationError(`${label} enabled must be boolean.`);
|
|
88
|
+
}
|
|
89
|
+
let channel;
|
|
90
|
+
try {
|
|
91
|
+
channel = validateChannel(value.channel ?? 'all');
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw withValidationDetail(error, { kind: 'channel' });
|
|
76
94
|
}
|
|
77
|
-
const channel = validateChannel(value.channel ?? 'all');
|
|
78
95
|
const parameterByName = new Map(definition.parameters.map(parameter => [parameter.name, parameter]));
|
|
79
96
|
for (const key of Object.keys(value.parameters)) {
|
|
80
97
|
if (!parameterByName.has(key)) {
|
|
81
|
-
throw
|
|
98
|
+
throw withValidationDetail(
|
|
99
|
+
new ValidationError(`Unknown parameter ${value.type}.${key}.`),
|
|
100
|
+
{ kind: 'parameter', parameter: key }
|
|
101
|
+
);
|
|
82
102
|
}
|
|
83
103
|
}
|
|
84
104
|
const parameters = {};
|
|
@@ -86,26 +106,34 @@ function fromPlainEffect(value, index) {
|
|
|
86
106
|
const supplied = Object.hasOwn(value.parameters, parameter.name)
|
|
87
107
|
? value.parameters[parameter.name]
|
|
88
108
|
: parameter.default;
|
|
89
|
-
|
|
109
|
+
try {
|
|
110
|
+
parameters[parameter.name] = validateParameterValue(value.type, parameter, supplied);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw withValidationDetail(error, { kind: 'parameter', parameter: parameter.name });
|
|
113
|
+
}
|
|
90
114
|
}
|
|
91
115
|
const declaredAssets = definition.assets ?? [];
|
|
92
116
|
let assets;
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
try {
|
|
118
|
+
if (declaredAssets.length === 0) {
|
|
119
|
+
if (value.assets !== undefined) throw new AssetError(`${value.type} does not accept external assets.`);
|
|
120
|
+
} else {
|
|
121
|
+
if (!isRecord(value.assets)) throw new AssetError(`${value.type} requires an assets object.`);
|
|
122
|
+
const allowed = new Set(declaredAssets.map(asset => asset.name));
|
|
123
|
+
for (const name of Object.keys(value.assets)) {
|
|
124
|
+
if (!allowed.has(name)) throw new AssetError(`${value.type} has no asset named ${name}.`);
|
|
125
|
+
}
|
|
126
|
+
assets = {};
|
|
127
|
+
for (const asset of declaredAssets) {
|
|
128
|
+
const reference = value.assets[asset.name];
|
|
129
|
+
if (asset.required && (typeof reference !== 'string' || reference.length < 1 || reference.length > 128)) {
|
|
130
|
+
throw new AssetError(`${value.type}.${asset.name} requires a non-empty asset reference.`);
|
|
131
|
+
}
|
|
132
|
+
if (reference !== undefined) assets[asset.name] = reference;
|
|
106
133
|
}
|
|
107
|
-
if (reference !== undefined) assets[asset.name] = reference;
|
|
108
134
|
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw withValidationDetail(error, { kind: 'assets' });
|
|
109
137
|
}
|
|
110
138
|
return { id, type: value.type, enabled, channel, parameters, ...(assets ? { assets } : {}) };
|
|
111
139
|
}
|
|
@@ -134,7 +162,7 @@ function assignIds(effects) {
|
|
|
134
162
|
});
|
|
135
163
|
}
|
|
136
164
|
|
|
137
|
-
export function normalizeChainDocument(input) {
|
|
165
|
+
export function normalizeChainDocument(input, { entryLabel } = {}) {
|
|
138
166
|
let entries;
|
|
139
167
|
if (Array.isArray(input)) {
|
|
140
168
|
entries = input;
|
|
@@ -149,7 +177,11 @@ export function normalizeChainDocument(input) {
|
|
|
149
177
|
entries = input.chain;
|
|
150
178
|
}
|
|
151
179
|
const effects = entries.map((entry, index) =>
|
|
152
|
-
fromPlainEffect(
|
|
180
|
+
fromPlainEffect(
|
|
181
|
+
entry instanceof Effect ? entry.toJSON() : entry,
|
|
182
|
+
index,
|
|
183
|
+
entryLabel ?? `Chain entry ${index}`
|
|
184
|
+
)
|
|
153
185
|
);
|
|
154
186
|
return {
|
|
155
187
|
version: 1,
|
|
@@ -250,14 +282,41 @@ export function packEffect(effect) {
|
|
|
250
282
|
export function setEffectParameter(effect, parameterName, value) {
|
|
251
283
|
const definition = getEffectDefinition(effect.type);
|
|
252
284
|
const parameter = definition.parameters.find(entry => entry.name === parameterName);
|
|
253
|
-
if (!parameter)
|
|
254
|
-
|
|
285
|
+
if (!parameter) {
|
|
286
|
+
throw withValidationDetail(
|
|
287
|
+
new ValidationError(`Unknown parameter ${effect.type}.${parameterName}.`),
|
|
288
|
+
{ kind: 'parameter', parameter: parameterName }
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
let validated;
|
|
292
|
+
try {
|
|
293
|
+
validated = validateParameterValue(effect.type, parameter, value);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
throw withValidationDetail(error, { kind: 'parameter', parameter: parameterName });
|
|
296
|
+
}
|
|
255
297
|
return {
|
|
256
298
|
...cloneEffect(effect),
|
|
257
299
|
parameters: { ...effect.parameters, [parameterName]: validated }
|
|
258
300
|
};
|
|
259
301
|
}
|
|
260
302
|
|
|
303
|
+
export function canonicalizeEffectForProcessing(effect) {
|
|
304
|
+
const canonical = cloneEffect(effect);
|
|
305
|
+
const parameters = canonical.parameters;
|
|
306
|
+
if (effect.type === 'AutoFilter' &&
|
|
307
|
+
parameters.minimumFrequency > parameters.maximumFrequency) {
|
|
308
|
+
[parameters.minimumFrequency, parameters.maximumFrequency] =
|
|
309
|
+
[parameters.maximumFrequency, parameters.minimumFrequency];
|
|
310
|
+
} else if (effect.type === 'Chorus' && parameters.depth > parameters.delay) {
|
|
311
|
+
parameters.depth = parameters.delay;
|
|
312
|
+
} else if (effect.type === 'FrequencyShifter' &&
|
|
313
|
+
parameters.minimumShift > parameters.maximumShift) {
|
|
314
|
+
[parameters.minimumShift, parameters.maximumShift] =
|
|
315
|
+
[parameters.maximumShift, parameters.minimumShift];
|
|
316
|
+
}
|
|
317
|
+
return canonical;
|
|
318
|
+
}
|
|
319
|
+
|
|
261
320
|
export function validateStreamParameterUpdate(effect, parameterName) {
|
|
262
321
|
if (STREAM_RECONFIGURATION_PARAMETERS.get(effect.type)?.has(parameterName)) {
|
|
263
322
|
throw new ValidationError(
|
|
@@ -8,6 +8,7 @@ class EffeTuneDspProcessor extends AudioWorkletProcessor {
|
|
|
8
8
|
this.session = null;
|
|
9
9
|
this.ready = false;
|
|
10
10
|
this.closed = false;
|
|
11
|
+
this.latencySamples = null;
|
|
11
12
|
this.channels = 0;
|
|
12
13
|
this.pendingCommands = [];
|
|
13
14
|
this.sourceChannels = [];
|
|
@@ -66,7 +67,8 @@ class EffeTuneDspProcessor extends AudioWorkletProcessor {
|
|
|
66
67
|
);
|
|
67
68
|
}
|
|
68
69
|
this.ready = true;
|
|
69
|
-
this.
|
|
70
|
+
this.latencySamples = this.session?.latencySamples ?? 0;
|
|
71
|
+
this.port.postMessage({ type: 'ready', latencySamples: this.latencySamples });
|
|
70
72
|
} catch (error) {
|
|
71
73
|
this.port.postMessage({
|
|
72
74
|
type: 'initializationError',
|
|
@@ -95,6 +97,11 @@ class EffeTuneDspProcessor extends AudioWorkletProcessor {
|
|
|
95
97
|
this.session?.setTelemetryEnabled(this.telemetryEnabled);
|
|
96
98
|
continue;
|
|
97
99
|
}
|
|
100
|
+
const latencySamples = this.session?.latencySamples ?? 0;
|
|
101
|
+
if (latencySamples !== this.latencySamples) {
|
|
102
|
+
this.latencySamples = latencySamples;
|
|
103
|
+
this.port.postMessage({ type: 'latency', latencySamples });
|
|
104
|
+
}
|
|
98
105
|
this.port.postMessage({ type: 'commandResult', commandId: command.commandId, ok: true });
|
|
99
106
|
} catch (error) {
|
|
100
107
|
this.port.postMessage({
|
package/dist/worklet.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export declare class EffeTuneNode extends AudioWorkletNode {
|
|
|
23
23
|
readonly (Effect | ChainEffectInput)[],
|
|
24
24
|
options?: EffeTuneNodeOptions
|
|
25
25
|
): Promise<EffeTuneNode>;
|
|
26
|
+
readonly latencySamples: number;
|
|
26
27
|
readonly droppedTelemetryFrames: number;
|
|
27
28
|
subscribe(callback: TelemetryCallback): () => void;
|
|
28
29
|
unsubscribe(callback: TelemetryCallback): boolean;
|
package/dist/worklet.js
CHANGED
|
@@ -184,6 +184,7 @@ export class EffeTuneNode extends AudioWorkletNodeBase {
|
|
|
184
184
|
this._seed = seed;
|
|
185
185
|
this._closed = false;
|
|
186
186
|
this._runtimeError = null;
|
|
187
|
+
this._latencySamples = 0;
|
|
187
188
|
this._nextCommandId = 1;
|
|
188
189
|
this._pending = new Map();
|
|
189
190
|
this._telemetryCallbacks = new Set();
|
|
@@ -237,9 +238,18 @@ export class EffeTuneNode extends AudioWorkletNodeBase {
|
|
|
237
238
|
return;
|
|
238
239
|
}
|
|
239
240
|
if (message?.type === 'ready') {
|
|
241
|
+
this._latencySamples = Number.isInteger(message.latencySamples) && message.latencySamples >= 0
|
|
242
|
+
? message.latencySamples
|
|
243
|
+
: 0;
|
|
240
244
|
this._resolveReady();
|
|
241
245
|
return;
|
|
242
246
|
}
|
|
247
|
+
if (message?.type === 'latency') {
|
|
248
|
+
this._latencySamples = Number.isInteger(message.latencySamples) && message.latencySamples >= 0
|
|
249
|
+
? message.latencySamples
|
|
250
|
+
: 0;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
243
253
|
if (message?.type === 'initializationError') {
|
|
244
254
|
this._rejectReady(errorFromMessage(
|
|
245
255
|
message.errorType,
|
|
@@ -315,6 +325,11 @@ export class EffeTuneNode extends AudioWorkletNodeBase {
|
|
|
315
325
|
return this._droppedTelemetryFrames;
|
|
316
326
|
}
|
|
317
327
|
|
|
328
|
+
get latencySamples() {
|
|
329
|
+
this._assertOpen();
|
|
330
|
+
return this._latencySamples;
|
|
331
|
+
}
|
|
332
|
+
|
|
318
333
|
subscribe(callback) {
|
|
319
334
|
this._assertOpen();
|
|
320
335
|
if (typeof callback !== 'function') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effetune/dsp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "EffeTune's deterministic WebAssembly audio effects for JavaScript and AudioWorklet",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"./processor": "./dist/worklet-processor.js",
|
|
39
39
|
"./schemas/chain-v1.json": "./dist/schemas/chain-v1.schema.json",
|
|
40
|
+
"./schemas/graph-v1.json": "./dist/schemas/graph-v1.schema.json",
|
|
40
41
|
"./schemas/bundle-v1.json": "./dist/schemas/bundle-v1.schema.json",
|
|
41
42
|
"./catalog": {
|
|
42
43
|
"types": "./dist/catalog-entry.d.ts",
|