@effetune/dsp 0.1.0 → 0.5.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 +106 -5
- package/dist/assets/effetune-dsp.meta.json +491 -7
- package/dist/assets/effetune-dsp.simd.wasm +0 -0
- package/dist/assets/effetune-dsp.wasm +0 -0
- package/dist/catalog/effects-v1.json +1778 -242
- package/dist/errors.js +21 -0
- package/dist/generated-effects.d.ts +317 -1
- package/dist/generated-effects.js +185 -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 +268 -1
- package/dist/index.js +36 -0
- package/dist/internal/dsp-engine-binding.js +162 -1
- package/dist/internal/dsp-params.generated.js +537 -70
- package/dist/internal/dsp-wasm-loader.js +2 -0
- package/dist/preset.js +29 -0
- package/dist/runtime.js +73 -5
- package/dist/schemas/chain-v1.schema.json +1740 -167
- package/dist/schemas/graph-v1.schema.json +8208 -0
- package/dist/semantics.js +88 -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']);
|
|
@@ -51,34 +57,47 @@ function cloneEffect(effect) {
|
|
|
51
57
|
};
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
function fromPlainEffect(value, index) {
|
|
55
|
-
if (!isRecord(value)) throw new ValidationError(
|
|
60
|
+
function fromPlainEffect(value, index, label = `Chain entry ${index}`) {
|
|
61
|
+
if (!isRecord(value)) throw new ValidationError(`${label} must be an object.`);
|
|
56
62
|
for (const key of Object.keys(value)) {
|
|
57
63
|
if (!EFFECT_KEYS.has(key)) {
|
|
58
|
-
throw new ValidationError(
|
|
64
|
+
throw new ValidationError(`${label} has an unsupported field: ${key}`);
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
if (typeof value.type !== 'string') {
|
|
62
|
-
throw new ValidationError(
|
|
68
|
+
throw new ValidationError(`${label} requires an effect type.`);
|
|
63
69
|
}
|
|
64
70
|
if (!isRecord(value.parameters)) {
|
|
65
|
-
throw new ValidationError(
|
|
71
|
+
throw new ValidationError(`${label} requires a parameters object.`);
|
|
72
|
+
}
|
|
73
|
+
let definition;
|
|
74
|
+
try {
|
|
75
|
+
definition = getEffectDefinition(value.type);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw withValidationDetail(error, { kind: 'type' });
|
|
66
78
|
}
|
|
67
|
-
const definition = getEffectDefinition(value.type);
|
|
68
79
|
const id = value.id;
|
|
69
80
|
if (id !== undefined &&
|
|
70
81
|
(typeof id !== 'string' || id.length < 1 || id.length > 128)) {
|
|
71
|
-
throw new ValidationError(
|
|
82
|
+
throw new ValidationError(`${label} has an invalid effect id.`);
|
|
72
83
|
}
|
|
73
84
|
const enabled = value.enabled ?? true;
|
|
74
85
|
if (typeof enabled !== 'boolean') {
|
|
75
|
-
throw new ValidationError(
|
|
86
|
+
throw new ValidationError(`${label} enabled must be boolean.`);
|
|
87
|
+
}
|
|
88
|
+
let channel;
|
|
89
|
+
try {
|
|
90
|
+
channel = validateChannel(value.channel ?? 'all');
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw withValidationDetail(error, { kind: 'channel' });
|
|
76
93
|
}
|
|
77
|
-
const channel = validateChannel(value.channel ?? 'all');
|
|
78
94
|
const parameterByName = new Map(definition.parameters.map(parameter => [parameter.name, parameter]));
|
|
79
95
|
for (const key of Object.keys(value.parameters)) {
|
|
80
96
|
if (!parameterByName.has(key)) {
|
|
81
|
-
throw
|
|
97
|
+
throw withValidationDetail(
|
|
98
|
+
new ValidationError(`Unknown parameter ${value.type}.${key}.`),
|
|
99
|
+
{ kind: 'parameter', parameter: key }
|
|
100
|
+
);
|
|
82
101
|
}
|
|
83
102
|
}
|
|
84
103
|
const parameters = {};
|
|
@@ -86,26 +105,34 @@ function fromPlainEffect(value, index) {
|
|
|
86
105
|
const supplied = Object.hasOwn(value.parameters, parameter.name)
|
|
87
106
|
? value.parameters[parameter.name]
|
|
88
107
|
: parameter.default;
|
|
89
|
-
|
|
108
|
+
try {
|
|
109
|
+
parameters[parameter.name] = validateParameterValue(value.type, parameter, supplied);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw withValidationDetail(error, { kind: 'parameter', parameter: parameter.name });
|
|
112
|
+
}
|
|
90
113
|
}
|
|
91
114
|
const declaredAssets = definition.assets ?? [];
|
|
92
115
|
let assets;
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
try {
|
|
117
|
+
if (declaredAssets.length === 0) {
|
|
118
|
+
if (value.assets !== undefined) throw new AssetError(`${value.type} does not accept external assets.`);
|
|
119
|
+
} else {
|
|
120
|
+
if (!isRecord(value.assets)) throw new AssetError(`${value.type} requires an assets object.`);
|
|
121
|
+
const allowed = new Set(declaredAssets.map(asset => asset.name));
|
|
122
|
+
for (const name of Object.keys(value.assets)) {
|
|
123
|
+
if (!allowed.has(name)) throw new AssetError(`${value.type} has no asset named ${name}.`);
|
|
124
|
+
}
|
|
125
|
+
assets = {};
|
|
126
|
+
for (const asset of declaredAssets) {
|
|
127
|
+
const reference = value.assets[asset.name];
|
|
128
|
+
if (asset.required && (typeof reference !== 'string' || reference.length < 1 || reference.length > 128)) {
|
|
129
|
+
throw new AssetError(`${value.type}.${asset.name} requires a non-empty asset reference.`);
|
|
130
|
+
}
|
|
131
|
+
if (reference !== undefined) assets[asset.name] = reference;
|
|
106
132
|
}
|
|
107
|
-
if (reference !== undefined) assets[asset.name] = reference;
|
|
108
133
|
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
throw withValidationDetail(error, { kind: 'assets' });
|
|
109
136
|
}
|
|
110
137
|
return { id, type: value.type, enabled, channel, parameters, ...(assets ? { assets } : {}) };
|
|
111
138
|
}
|
|
@@ -134,7 +161,7 @@ function assignIds(effects) {
|
|
|
134
161
|
});
|
|
135
162
|
}
|
|
136
163
|
|
|
137
|
-
export function normalizeChainDocument(input) {
|
|
164
|
+
export function normalizeChainDocument(input, { entryLabel } = {}) {
|
|
138
165
|
let entries;
|
|
139
166
|
if (Array.isArray(input)) {
|
|
140
167
|
entries = input;
|
|
@@ -149,7 +176,11 @@ export function normalizeChainDocument(input) {
|
|
|
149
176
|
entries = input.chain;
|
|
150
177
|
}
|
|
151
178
|
const effects = entries.map((entry, index) =>
|
|
152
|
-
fromPlainEffect(
|
|
179
|
+
fromPlainEffect(
|
|
180
|
+
entry instanceof Effect ? entry.toJSON() : entry,
|
|
181
|
+
index,
|
|
182
|
+
entryLabel ?? `Chain entry ${index}`
|
|
183
|
+
)
|
|
153
184
|
);
|
|
154
185
|
return {
|
|
155
186
|
version: 1,
|
|
@@ -250,14 +281,41 @@ export function packEffect(effect) {
|
|
|
250
281
|
export function setEffectParameter(effect, parameterName, value) {
|
|
251
282
|
const definition = getEffectDefinition(effect.type);
|
|
252
283
|
const parameter = definition.parameters.find(entry => entry.name === parameterName);
|
|
253
|
-
if (!parameter)
|
|
254
|
-
|
|
284
|
+
if (!parameter) {
|
|
285
|
+
throw withValidationDetail(
|
|
286
|
+
new ValidationError(`Unknown parameter ${effect.type}.${parameterName}.`),
|
|
287
|
+
{ kind: 'parameter', parameter: parameterName }
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
let validated;
|
|
291
|
+
try {
|
|
292
|
+
validated = validateParameterValue(effect.type, parameter, value);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
throw withValidationDetail(error, { kind: 'parameter', parameter: parameterName });
|
|
295
|
+
}
|
|
255
296
|
return {
|
|
256
297
|
...cloneEffect(effect),
|
|
257
298
|
parameters: { ...effect.parameters, [parameterName]: validated }
|
|
258
299
|
};
|
|
259
300
|
}
|
|
260
301
|
|
|
302
|
+
export function canonicalizeEffectForProcessing(effect) {
|
|
303
|
+
const canonical = cloneEffect(effect);
|
|
304
|
+
const parameters = canonical.parameters;
|
|
305
|
+
if (effect.type === 'AutoFilter' &&
|
|
306
|
+
parameters.minimumFrequency > parameters.maximumFrequency) {
|
|
307
|
+
[parameters.minimumFrequency, parameters.maximumFrequency] =
|
|
308
|
+
[parameters.maximumFrequency, parameters.minimumFrequency];
|
|
309
|
+
} else if (effect.type === 'Chorus' && parameters.depth > parameters.delay) {
|
|
310
|
+
parameters.depth = parameters.delay;
|
|
311
|
+
} else if (effect.type === 'FrequencyShifter' &&
|
|
312
|
+
parameters.minimumShift > parameters.maximumShift) {
|
|
313
|
+
[parameters.minimumShift, parameters.maximumShift] =
|
|
314
|
+
[parameters.maximumShift, parameters.minimumShift];
|
|
315
|
+
}
|
|
316
|
+
return canonical;
|
|
317
|
+
}
|
|
318
|
+
|
|
261
319
|
export function validateStreamParameterUpdate(effect, parameterName) {
|
|
262
320
|
if (STREAM_RECONFIGURATION_PARAMETERS.get(effect.type)?.has(parameterName)) {
|
|
263
321
|
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.5.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",
|