@effetune/dsp 0.4.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.
@@ -0,0 +1,493 @@
1
+ import { instantiateDspBinding, ET_OK } from './internal/dsp-engine-binding.js';
2
+ import { getEffectImplementation } from './catalog.js';
3
+ import { prepareConvolutionAsset } from './assets.js';
4
+ import { AssetError, EffeTuneError, EffeTuneRuntimeError, ValidationError } from './errors.js';
5
+ import { channelRange, packEffect } from './semantics.js';
6
+ import { TELEMETRY_RING_BYTES } from './telemetry.js';
7
+ import { GRAPH_V1_CAPACITY } from './generated-graph-contract.js';
8
+
9
+ const GRAPH_MAGIC = 0x31475445;
10
+ const SNAPSHOT_MAGIC = 0x31535445;
11
+ const ENDPOINT = 0xffffffff;
12
+ const UNASSIGNED_SLOT = 0xffffffff;
13
+
14
+ function requireOk(status, operation) {
15
+ if (status !== ET_OK) throw new EffeTuneRuntimeError(`DSP ${operation} failed.`);
16
+ }
17
+
18
+ function instancePrepareError(state, effect, message, cause) {
19
+ return new EffeTuneRuntimeError(message, {
20
+ code: 'GRAPH_INSTANCE_PREPARE',
21
+ path: `/nodes/${state.originalNodeIndexes.get(effect.id)}`,
22
+ nodeId: effect.id,
23
+ cause
24
+ });
25
+ }
26
+
27
+ function channelSpec(channel) {
28
+ if (channel === 'all') return -2;
29
+ if (channel === 'stereo') return -1;
30
+ if (channel === 'left' || channel === '1') return 0;
31
+ if (channel === 'right' || channel === '2') return 1;
32
+ if (/^[3-8]$/.test(channel)) return Number(channel) - 1;
33
+ return { '34': 17, '56': 18, '78': 19 }[channel];
34
+ }
35
+
36
+ export function effectiveNodeIds(document) {
37
+ const soloGroups = new Set(
38
+ document.edges.filter(edge => edge.solo).map(
39
+ edge => `${edge.destination}\0${edge.mixGroup}`
40
+ )
41
+ );
42
+ const active = document.edges.filter(edge =>
43
+ !edge.mute && (!soloGroups.has(`${edge.destination}\0${edge.mixGroup}`) || edge.solo)
44
+ );
45
+ const incoming = new Map();
46
+ for (const edge of active) {
47
+ const edges = incoming.get(edge.destination) ?? [];
48
+ edges.push(edge);
49
+ incoming.set(edge.destination, edges);
50
+ }
51
+ const result = new Set();
52
+ const visited = new Set([document.output.id]);
53
+ const queue = [document.output.id];
54
+ while (queue.length > 0) {
55
+ for (const edge of incoming.get(queue.shift()) ?? []) {
56
+ if (document.nodes.some(node => node.id === edge.source)) result.add(edge.source);
57
+ if (!visited.has(edge.source)) {
58
+ visited.add(edge.source);
59
+ queue.push(edge.source);
60
+ }
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+
66
+ function appendString(chunks, offsets, value) {
67
+ const bytes = new TextEncoder().encode(value);
68
+ const offset = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
69
+ chunks.push(bytes);
70
+ offsets.push({ offset, length: bytes.length });
71
+ }
72
+
73
+ export function encodeGraphDescriptor(document, instanceIds, assetNodeIds = new Set()) {
74
+ const strings = [];
75
+ const nodeStrings = [];
76
+ const edgeStrings = [];
77
+ for (const node of document.nodes) appendString(strings, nodeStrings, node.id);
78
+ for (const edge of document.edges) {
79
+ const fields = [];
80
+ appendString(strings, fields, edge.id);
81
+ appendString(strings, fields, edge.mixGroup);
82
+ edgeStrings.push(fields);
83
+ }
84
+ const stringBytes = strings.reduce((sum, chunk) => sum + chunk.length, 0);
85
+ const nodeOffset = 32;
86
+ const edgeOffset = nodeOffset + document.nodes.length * 24;
87
+ const stringOffset = edgeOffset + document.edges.length * 40;
88
+ const bytes = new Uint8Array(stringOffset + stringBytes);
89
+ const view = new DataView(bytes.buffer);
90
+ view.setUint32(0, GRAPH_MAGIC, true);
91
+ view.setUint32(4, 1, true);
92
+ view.setUint32(8, document.nodes.length, true);
93
+ view.setUint32(12, document.edges.length, true);
94
+ view.setUint32(16, stringBytes, true);
95
+ const nodeIndexes = new Map(document.nodes.map((node, index) => [node.id, index]));
96
+ for (const [index, node] of document.nodes.entries()) {
97
+ const offset = nodeOffset + index * 24;
98
+ const string = nodeStrings[index];
99
+ let flags = 0;
100
+ if (node.enabled) {
101
+ flags = 1;
102
+ if (assetNodeIds.has(node.id)) flags |= 2;
103
+ if (node.type !== 'IRReverb' || node.parameters.dryEnabled === false ||
104
+ node.parameters.dryLevel <= -96) flags |= 4;
105
+ }
106
+ view.setUint32(offset, instanceIds.get(node.id) ?? 0, true);
107
+ view.setUint32(offset + 4, string.offset, true);
108
+ view.setUint32(offset + 8, string.length, true);
109
+ view.setUint32(offset + 12, flags, true);
110
+ view.setInt32(offset + 16, channelSpec(node.channel), true);
111
+ }
112
+ for (const [index, edge] of document.edges.entries()) {
113
+ const offset = edgeOffset + index * 40;
114
+ const [id, mixGroup] = edgeStrings[index];
115
+ let flags = (edge.mute ? 1 : 0) | (edge.solo ? 2 : 0);
116
+ if (Object.hasOwn(edge, 'pan')) flags |= 4;
117
+ view.setUint32(offset, edge.source === document.input.id
118
+ ? ENDPOINT : nodeIndexes.get(edge.source), true);
119
+ view.setUint32(offset + 4, edge.destination === document.output.id
120
+ ? ENDPOINT : nodeIndexes.get(edge.destination), true);
121
+ view.setUint32(offset + 8, id.offset, true);
122
+ view.setUint32(offset + 12, id.length, true);
123
+ view.setUint32(offset + 16, mixGroup.offset, true);
124
+ view.setUint32(offset + 20, mixGroup.length, true);
125
+ view.setFloat32(offset + 24, edge.gain, true);
126
+ view.setFloat32(offset + 28, edge.pan ?? 0, true);
127
+ view.setUint32(offset + 32, flags, true);
128
+ }
129
+ let cursor = stringOffset;
130
+ for (const chunk of strings) {
131
+ bytes.set(chunk, cursor);
132
+ cursor += chunk.length;
133
+ }
134
+ return bytes;
135
+ }
136
+
137
+ function vector(view, offset, channels) {
138
+ return Array.from({ length: channels }, (_, index) => view.getUint32(offset + index * 4, true));
139
+ }
140
+
141
+ function optionalSlot(value) {
142
+ return value === UNASSIGNED_SLOT ? null : value;
143
+ }
144
+
145
+ export function decodeGraphSnapshot(bytes, document) {
146
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength < 128) {
147
+ throw new EffeTuneRuntimeError('DSP returned an invalid Graph compile snapshot.');
148
+ }
149
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
150
+ if (view.getUint32(0, true) !== SNAPSHOT_MAGIC || view.getUint32(4, true) !== 1) {
151
+ throw new EffeTuneRuntimeError('DSP returned an unsupported Graph compile snapshot.');
152
+ }
153
+ const nodeCount = view.getUint32(8, true);
154
+ const edgeCount = view.getUint32(12, true);
155
+ const scheduleCount = view.getUint32(16, true);
156
+ const channels = view.getUint32(20, true);
157
+ const nodeBytes = view.getUint32(40, true);
158
+ const edgeBytes = view.getUint32(44, true);
159
+ const scheduleOffset = view.getUint32(48, true);
160
+ const nodesOffset = view.getUint32(52, true);
161
+ const edgesOffset = view.getUint32(56, true);
162
+ const totalBytes = view.getUint32(60, true);
163
+ if (nodeCount !== document.nodes.length || edgeCount !== document.edges.length ||
164
+ channels < 1 || channels > 8 || nodeBytes !== 128 || edgeBytes !== 48 ||
165
+ totalBytes !== bytes.byteLength) {
166
+ throw new EffeTuneRuntimeError('DSP returned an inconsistent Graph compile snapshot.');
167
+ }
168
+ const schedule = Array.from({ length: scheduleCount }, (_, index) =>
169
+ document.nodes[view.getUint32(scheduleOffset + index * 4, true)]?.id
170
+ );
171
+ const nodes = document.nodes.map((node, index) => {
172
+ const offset = nodesOffset + index * nodeBytes;
173
+ const flags = view.getUint32(offset, true);
174
+ return {
175
+ id: node.id,
176
+ effective: (flags & 1) !== 0,
177
+ dormant: (flags & 2) !== 0,
178
+ enabled: (flags & 4) !== 0,
179
+ disabledBypass: (flags & 8) !== 0,
180
+ scheduleIndex: optionalSlot(view.getUint32(offset + 4, true)),
181
+ bufferSlot: optionalSlot(view.getUint32(offset + 8, true)),
182
+ processingGroup: {
183
+ firstChannel: view.getUint32(offset + 16, true),
184
+ channelCount: view.getUint32(offset + 20, true)
185
+ },
186
+ kernelLatency: view.getUint32(offset + 24, true),
187
+ inputLatency: vector(view, offset + 32, channels),
188
+ outputLatency: vector(view, offset + 64, channels),
189
+ preNodeCompensation: vector(view, offset + 96, channels)
190
+ };
191
+ });
192
+ const edges = document.edges.map((edge, index) => {
193
+ const offset = edgesOffset + index * edgeBytes;
194
+ const flags = view.getUint32(offset, true);
195
+ return {
196
+ id: edge.id,
197
+ active: (flags & 1) !== 0,
198
+ suppressed: (flags & 2) !== 0,
199
+ dormant: (flags & 4) !== 0,
200
+ fanInCompensation: vector(view, offset + 16, channels)
201
+ };
202
+ });
203
+ const flags = view.getUint32(36, true);
204
+ return Object.freeze({
205
+ version: 1,
206
+ identity: (flags & 1) !== 0,
207
+ silence: (flags & 2) !== 0,
208
+ effectiveSchedule: schedule,
209
+ nodes,
210
+ edges,
211
+ outputLatency: vector(view, 64, channels),
212
+ outputCompensation: vector(view, 96, channels),
213
+ latencySamples: view.getUint32(28, true),
214
+ capacity: {
215
+ bufferSlots: view.getUint32(24, true),
216
+ workspaceBytes: view.getUint32(32, true)
217
+ }
218
+ });
219
+ }
220
+
221
+ const STATUS_CODES = new Map([
222
+ [-9, 'GRAPH_DOCUMENT_CYCLE'],
223
+ [-10, 'GRAPH_DOCUMENT_CONNECTIVITY'],
224
+ [-11, 'GRAPH_CAPACITY'],
225
+ [-14, 'GRAPH_INSTANCE_PREPARE'],
226
+ [-12, 'GRAPH_LATENCY_OVERFLOW'],
227
+ [-13, 'GRAPH_UNSUPPORTED_CAPABILITY'],
228
+ [-3, 'GRAPH_PLAN_MEMORY']
229
+ ]);
230
+ const NODE_PATHS = new Map([[2, '/id'], [3, ''], [4, '/assets'], [5, ''], [6, '/channel']]);
231
+ const EDGE_PATHS = new Map([[7, '/id'], [8, '/source'], [9, '/destination'], [10, '/gain'], [11, '/pan'], [12, '']]);
232
+
233
+ function invalidGraphCode(diagnostic) {
234
+ if (diagnostic?.path === 2 || diagnostic?.path === 7) return 'GRAPH_DOCUMENT_ID';
235
+ if (diagnostic?.path === 6) return 'GRAPH_DOCUMENT_CHANNEL';
236
+ if (diagnostic?.path === 8 || diagnostic?.path === 9 || diagnostic?.path === 1) {
237
+ return 'GRAPH_DOCUMENT_REFERENCE';
238
+ }
239
+ if (diagnostic?.path === 10 || diagnostic?.path === 11 || diagnostic?.path === 12) {
240
+ return 'GRAPH_DOCUMENT_EDGE_CONTROL';
241
+ }
242
+ if (diagnostic?.path === 3 || diagnostic?.path === 4) return 'GRAPH_INSTANCE_PREPARE';
243
+ if (diagnostic?.path === 5) return 'GRAPH_UNSUPPORTED_CAPABILITY';
244
+ if (diagnostic?.path === 15) return 'GRAPH_CAPACITY';
245
+ if (diagnostic?.path === 16 || diagnostic?.path === 18) return 'GRAPH_LATENCY_OVERFLOW';
246
+ if (diagnostic?.path === 17) return 'GRAPH_PLAN_MEMORY';
247
+ return 'GRAPH_DOCUMENT_REFERENCE';
248
+ }
249
+
250
+ export function graphCompileError(status, diagnostic, state) {
251
+ const code = status === -8
252
+ ? invalidGraphCode(diagnostic)
253
+ : STATUS_CODES.get(status) ?? 'GRAPH_PLAN_MEMORY';
254
+ let path = '';
255
+ let nodeId;
256
+ let edgeId;
257
+ let nodeType;
258
+ if (diagnostic?.kind === 1) {
259
+ const node = state.document.nodes[diagnostic.index];
260
+ nodeId = node?.id;
261
+ nodeType = node?.type;
262
+ const original = state.originalNodeIndexes.get(nodeId);
263
+ path = `/nodes/${original ?? diagnostic.index}${NODE_PATHS.get(diagnostic.path) ?? ''}`;
264
+ if (code === 'GRAPH_UNSUPPORTED_CAPABILITY' && node?.type === 'IRReverb') {
265
+ path = `/nodes/${original ?? diagnostic.index}/parameters/dryLevel`;
266
+ }
267
+ } else if (diagnostic?.kind === 2) {
268
+ const edge = state.document.edges[diagnostic.index];
269
+ edgeId = edge?.id;
270
+ const original = state.originalEdgeIndexes.get(edgeId);
271
+ path = `/edges/${original ?? diagnostic.index}${EDGE_PATHS.get(diagnostic.path) ?? ''}`;
272
+ if (diagnostic.path === 12 && edge?.mixGroup === '') path += '/mixGroup';
273
+ }
274
+ // A hand-written document that trips an unsupported capability is the same caller
275
+ // mistake the recipe builders reject up front, so it reports the same error type and
276
+ // the same remedy instead of a generic "could not be prepared" runtime failure.
277
+ const unsupported = code === 'GRAPH_UNSUPPORTED_CAPABILITY';
278
+ const ErrorType = code.startsWith('GRAPH_DOCUMENT_') || unsupported
279
+ ? ValidationError
280
+ : EffeTuneRuntimeError;
281
+ let message = 'The DSP Graph could not be prepared.';
282
+ if (unsupported) {
283
+ message = nodeType === 'IRReverb'
284
+ ? 'IRReverb must be wet-only in a Graph; turn dryEnabled off or set dryLevel to -96 dB (the parameter minimum), then use the external dry edge.'
285
+ : 'The DSP Graph requires a capability this build does not support.';
286
+ }
287
+ return new ErrorType(message, { code, path, nodeId, edgeId });
288
+ }
289
+
290
+ export async function createGraphEngineSession(artifact, state, resolvedAssets, {
291
+ sampleRate,
292
+ channels,
293
+ maxFrames,
294
+ seed
295
+ }) {
296
+ let binding;
297
+ const nodes = [];
298
+ try {
299
+ const maximumNodes = GRAPH_V1_CAPACITY.maxStructuralNodes;
300
+ if (state.document.nodes.length > maximumNodes) {
301
+ const overflow = state.document.nodes.find(
302
+ node => state.originalNodeIndexes.get(node.id) === maximumNodes
303
+ );
304
+ throw new EffeTuneRuntimeError(
305
+ `The DSP Graph exceeds the structural node capacity (${maximumNodes}).`,
306
+ { code: 'GRAPH_CAPACITY', path: `/nodes/${maximumNodes}`, nodeId: overflow?.id }
307
+ );
308
+ }
309
+ const maximumEdges = GRAPH_V1_CAPACITY.maxEdges;
310
+ if (state.document.edges.length > maximumEdges) {
311
+ const overflow = state.document.edges.find(
312
+ edge => state.originalEdgeIndexes.get(edge.id) === maximumEdges
313
+ );
314
+ throw new EffeTuneRuntimeError(
315
+ `The DSP Graph exceeds the edge capacity (${maximumEdges}).`,
316
+ { code: 'GRAPH_CAPACITY', path: `/edges/${maximumEdges}`, edgeId: overflow?.id }
317
+ );
318
+ }
319
+ const effective = effectiveNodeIds(state.document);
320
+ const effectiveNodes = state.document.nodes.filter(
321
+ effect => effect.enabled && effective.has(effect.id)
322
+ );
323
+ if (effectiveNodes.length > GRAPH_V1_CAPACITY.maxEffectiveInstances) {
324
+ const effect = effectiveNodes[GRAPH_V1_CAPACITY.maxEffectiveInstances];
325
+ throw new EffeTuneRuntimeError('The DSP Graph exceeds the effective instance capacity.', {
326
+ code: 'GRAPH_CAPACITY',
327
+ path: `/nodes/${state.originalNodeIndexes.get(effect.id)}`,
328
+ nodeId: effect.id
329
+ });
330
+ }
331
+ binding = await instantiateDspBinding(artifact.module ?? artifact.bytes ?? artifact, {
332
+ warning: () => {}
333
+ });
334
+ if (!binding.graphSupported) {
335
+ throw new EffeTuneRuntimeError('This DSP artifact does not support Graph v1.', {
336
+ code: 'GRAPH_UNSUPPORTED_CAPABILITY', path: ''
337
+ });
338
+ }
339
+ binding.createEngine();
340
+ requireOk(binding.prepare(sampleRate, channels, maxFrames, TELEMETRY_RING_BYTES), 'preparation');
341
+ requireOk(binding.setTelemetryRate(0), 'telemetry configuration');
342
+ const instanceIds = new Map();
343
+ const assetNodeIds = new Set();
344
+ let tapId = 1;
345
+ for (const [nodeIndex, effect] of state.document.nodes.entries()) {
346
+ if (!effect.enabled || !effective.has(effect.id)) continue;
347
+ const packed = packEffect(effect);
348
+ const instanceId = binding.createInstance(packed.internalType);
349
+ if (!instanceId) {
350
+ throw instancePrepareError(state, effect, `Unable to create ${effect.type}.`);
351
+ }
352
+ instanceIds.set(effect.id, instanceId);
353
+ requireOk(binding.instanceSetTap(instanceId, tapId), `${effect.type} telemetry mapping`);
354
+ requireOk(binding.instanceSetSeed(instanceId, seed), `${effect.type} seed configuration`);
355
+ requireOk(binding.instanceSetParams(instanceId, packed.values, packed.hash), `${effect.type} parameter configuration`);
356
+ if (packed.bytes) requireOk(binding.instanceSetParamBytes(instanceId, packed.bytes, packed.hash), `${effect.type} structured parameter configuration`);
357
+ const implementation = getEffectImplementation(effect.type);
358
+ if (implementation.assets?.length) {
359
+ assetNodeIds.add(effect.id);
360
+ let prepared;
361
+ try {
362
+ prepared = prepareConvolutionAsset(effect, resolvedAssets, { sampleRate, engineChannels: channels });
363
+ } catch (error) {
364
+ if (error instanceof AssetError) {
365
+ throw new AssetError(error.message, {
366
+ code: 'GRAPH_INSTANCE_PREPARE',
367
+ path: `/nodes/${state.originalNodeIndexes.get(effect.id)}/assets`,
368
+ nodeId: effect.id,
369
+ cause: error
370
+ });
371
+ }
372
+ throw error;
373
+ }
374
+ const slot = implementation.assets.find(asset => asset.publicName === 'impulseResponse')?.slot ?? 0;
375
+ if (binding.instanceSetAsset(
376
+ instanceId,
377
+ slot,
378
+ prepared.payload,
379
+ prepared.beginInfo,
380
+ prepared.formatTag
381
+ ) !== ET_OK) {
382
+ throw new EffeTuneRuntimeError(`${effect.type} asset could not be prepared.`, {
383
+ code: 'GRAPH_INSTANCE_PREPARE',
384
+ path: `/nodes/${state.originalNodeIndexes.get(effect.id)}/assets`,
385
+ nodeId: effect.id
386
+ });
387
+ }
388
+ const warmupFrames = 128;
389
+ const silence = binding.getArenaViews().scratch.allChannels.subarray(0, prepared.beginInfo.processingChannels * warmupFrames);
390
+ const silencePtr = binding.pointerForArenaView(silence);
391
+ let assetState = binding.instanceAssetState(instanceId, slot);
392
+ const maximumWarmupBlocks = Math.ceil(2 * sampleRate / warmupFrames);
393
+ for (let block = 0; (assetState & 0xff) === 2 && block < maximumWarmupBlocks; block++) {
394
+ silence.fill(0);
395
+ requireOk(binding.instanceProcess(instanceId, silencePtr, prepared.beginInfo.processingChannels, warmupFrames, block * warmupFrames / sampleRate), `${effect.type} asset prewarming`);
396
+ assetState = binding.instanceAssetState(instanceId, slot);
397
+ }
398
+ if ((assetState & 0xff) !== 3) {
399
+ throw new EffeTuneRuntimeError(`${effect.type} asset did not become active.`, {
400
+ code: 'GRAPH_INSTANCE_PREPARE',
401
+ path: `/nodes/${state.originalNodeIndexes.get(effect.id)}/assets`,
402
+ nodeId: effect.id
403
+ });
404
+ }
405
+ requireOk(binding.resetInstance(instanceId), `${effect.type} post-prewarm reset`);
406
+ requireOk(binding.instanceSetSeed(instanceId, seed), `${effect.type} post-prewarm seed reset`);
407
+ requireOk(binding.instanceSetParams(instanceId, packed.values, packed.hash), `${effect.type} post-prewarm parameter reset`);
408
+ if (packed.bytes) requireOk(binding.instanceSetParamBytes(instanceId, packed.bytes, packed.hash), `${effect.type} post-prewarm structured parameter reset`);
409
+ }
410
+ nodes.push({
411
+ effectId: effect.id,
412
+ effectType: effect.type,
413
+ nodeIndex,
414
+ instanceId,
415
+ tapId: tapId++,
416
+ range: channelRange(effect.channel, channels),
417
+ initialValues: new Float32Array(packed.values),
418
+ initialBytes: packed.bytes ? new Uint8Array(packed.bytes) : null,
419
+ hash: packed.hash
420
+ });
421
+ }
422
+ const descriptor = encodeGraphDescriptor(state.document, instanceIds, assetNodeIds);
423
+ const status = binding.graphConfigure(descriptor);
424
+ if (status !== ET_OK) throw graphCompileError(status, binding.graphDiagnostic(), state);
425
+ const snapshot = decodeGraphSnapshot(binding.graphSnapshot(), state.document);
426
+ return new GraphEngineSession(binding, nodes, snapshot, { channels, maxFrames, seed });
427
+ } catch (error) {
428
+ binding?.close();
429
+ if (error instanceof EffeTuneError) throw error;
430
+ throw new EffeTuneRuntimeError('Unable to create the DSP Graph processing state.', { cause: error });
431
+ }
432
+ }
433
+
434
+ export class GraphEngineSession {
435
+ constructor(binding, nodes, snapshot, { channels, maxFrames, seed }) {
436
+ this.binding = binding;
437
+ this.nodes = nodes;
438
+ this.snapshot = snapshot;
439
+ this.channels = channels;
440
+ this.maxFrames = maxFrames;
441
+ this.seed = seed;
442
+ this.closed = false;
443
+ this.arena = binding.getArenaViews().combined;
444
+ this.arenaByteOffset = this.arena.byteOffset;
445
+ this.fullChannelViews = Array.from({ length: channels }, (_, channel) =>
446
+ this.arena.subarray(channel * maxFrames, (channel + 1) * maxFrames)
447
+ );
448
+ }
449
+
450
+ get latencySamples() {
451
+ if (this.closed) throw new EffeTuneRuntimeError('DSP Graph processing state is closed.');
452
+ return this.binding.graphLatency();
453
+ }
454
+
455
+ process(input, output, offset, frameCount, sampleRate, timeFrame = offset) {
456
+ if (this.closed) throw new EffeTuneRuntimeError('DSP Graph processing state is closed.');
457
+ for (let channel = 0; channel < this.channels; channel++) {
458
+ const target = frameCount === this.maxFrames
459
+ ? this.fullChannelViews[channel]
460
+ : this.arena.subarray(channel * frameCount, (channel + 1) * frameCount);
461
+ target.set(input[channel].subarray(offset, offset + frameCount));
462
+ }
463
+ requireOk(this.binding.graphProcess(this.channels, frameCount, timeFrame / sampleRate), 'Graph processing');
464
+ for (let channel = 0; channel < this.channels; channel++) {
465
+ const source = frameCount === this.maxFrames
466
+ ? this.fullChannelViews[channel]
467
+ : this.arena.subarray(channel * frameCount, (channel + 1) * frameCount);
468
+ output[channel].set(source, offset);
469
+ }
470
+ }
471
+
472
+ setPacked(effectId, values, hash, changedIndex) {
473
+ const node = this.nodes.find(entry => entry.effectId === effectId);
474
+ if (!node) throw new ValidationError(`Graph node ${effectId} is not effective.`, {
475
+ code: 'GRAPH_RECONFIGURATION_REQUIRED', nodeId: effectId
476
+ });
477
+ return this.binding.graphSetInstanceParams(node.instanceId, values, hash, changedIndex);
478
+ }
479
+
480
+ hasEffectiveNode(effectId) {
481
+ return this.nodes.some(entry => entry.effectId === effectId);
482
+ }
483
+
484
+ reset() {
485
+ requireOk(this.binding.graphReset(), 'Graph reset');
486
+ }
487
+
488
+ close() {
489
+ if (this.closed) return;
490
+ this.closed = true;
491
+ this.binding.close();
492
+ }
493
+ }