@effetune/dsp 0.0.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/LICENSE +21 -0
- package/README.md +186 -0
- package/THIRD_PARTY_NOTICES.txt +11 -0
- package/dist/artifacts.js +185 -0
- package/dist/assets/NOTICE.txt +30 -0
- package/dist/assets/effetune-dsp.meta.json +502 -0
- package/dist/assets/effetune-dsp.simd.wasm +0 -0
- package/dist/assets/effetune-dsp.wasm +0 -0
- package/dist/assets.js +767 -0
- package/dist/catalog/effects-v1.json +5369 -0
- package/dist/catalog-entry.d.ts +9 -0
- package/dist/catalog-entry.js +5 -0
- package/dist/catalog.js +50 -0
- package/dist/effect.d.ts +23 -0
- package/dist/effect.js +167 -0
- package/dist/engine.js +291 -0
- package/dist/errors.js +31 -0
- package/dist/generated-effects.d.ts +1460 -0
- package/dist/generated-effects.js +1007 -0
- package/dist/index.d.ts +512 -0
- package/dist/index.js +175 -0
- package/dist/internal/dsp-engine-binding.js +859 -0
- package/dist/internal/dsp-params.generated.js +1379 -0
- package/dist/internal/dsp-wasm-loader.js +346 -0
- package/dist/internal/ir-asset-payload.js +98 -0
- package/dist/internal/ir-plugin-contract.js +265 -0
- package/dist/preset.js +518 -0
- package/dist/runtime.js +488 -0
- package/dist/schemas/bundle-v1.schema.json +223 -0
- package/dist/schemas/chain-v1.schema.json +6530 -0
- package/dist/semantics.js +309 -0
- package/dist/telemetry.js +322 -0
- package/dist/worklet-processor.js +175 -0
- package/dist/worklet.d.ts +32 -0
- package/dist/worklet.js +386 -0
- package/package.json +53 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EffectChannel } from './generated-effects.js';
|
|
2
|
+
|
|
3
|
+
export declare function getEffectCatalog(): Readonly<{
|
|
4
|
+
version: 1;
|
|
5
|
+
channels: readonly EffectChannel[];
|
|
6
|
+
effects: readonly Readonly<Record<string, unknown>>[];
|
|
7
|
+
}>;
|
|
8
|
+
|
|
9
|
+
export declare const EFFECT_CATALOG: ReturnType<typeof getEffectCatalog>;
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as generated from './generated-effects.js';
|
|
2
|
+
import { EffectError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
let publicCatalog;
|
|
5
|
+
let publicByType;
|
|
6
|
+
|
|
7
|
+
function cloneJson(value) {
|
|
8
|
+
return JSON.parse(JSON.stringify(value));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function deepFreeze(value) {
|
|
12
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
13
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
14
|
+
return Object.freeze(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function metadata() {
|
|
18
|
+
const value = generated.EFFECT_METADATA;
|
|
19
|
+
if (!value || value.version !== 1 || !Array.isArray(value.effects)) {
|
|
20
|
+
throw new EffectError('The effect catalog is unavailable or incompatible.');
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ensureCatalog() {
|
|
26
|
+
if (!publicCatalog) {
|
|
27
|
+
publicCatalog = deepFreeze(cloneJson(metadata()));
|
|
28
|
+
publicByType = new Map(publicCatalog.effects.map(effect => [effect.type, effect]));
|
|
29
|
+
}
|
|
30
|
+
return publicCatalog;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getEffectCatalog() {
|
|
34
|
+
return ensureCatalog();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getEffectDefinition(type) {
|
|
38
|
+
ensureCatalog();
|
|
39
|
+
const effect = publicByType.get(type);
|
|
40
|
+
if (!effect) throw new EffectError(`Unknown effect type: ${String(type)}`);
|
|
41
|
+
return effect;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getEffectImplementation(type) {
|
|
45
|
+
const implementation = generated._EFFECT_IMPLEMENTATION?.[type];
|
|
46
|
+
if (!implementation) {
|
|
47
|
+
throw new EffectError(`The ${String(type)} effect is unavailable in this build.`);
|
|
48
|
+
}
|
|
49
|
+
return implementation;
|
|
50
|
+
}
|
package/dist/effect.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EffectChannel, EffectType } from './generated-effects.js';
|
|
2
|
+
|
|
3
|
+
export declare const EFFECT_CHANNELS: readonly EffectChannel[];
|
|
4
|
+
|
|
5
|
+
export declare class Effect {
|
|
6
|
+
readonly type: EffectType;
|
|
7
|
+
readonly id?: string;
|
|
8
|
+
readonly enabled: boolean;
|
|
9
|
+
readonly channel: EffectChannel;
|
|
10
|
+
readonly parameters: Readonly<Record<string, unknown>>;
|
|
11
|
+
readonly assets?: Readonly<Record<string, string>>;
|
|
12
|
+
constructor(type: EffectType, options?: Readonly<Record<string, unknown>>);
|
|
13
|
+
toJSON(): EffectDefinition;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EffectDefinition {
|
|
17
|
+
readonly id?: string;
|
|
18
|
+
readonly type: EffectType;
|
|
19
|
+
readonly enabled?: boolean;
|
|
20
|
+
readonly channel?: EffectChannel;
|
|
21
|
+
readonly parameters: Readonly<Record<string, unknown>>;
|
|
22
|
+
readonly assets?: Readonly<Record<string, string>>;
|
|
23
|
+
}
|
package/dist/effect.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { AssetError, ValidationError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
export const EFFECT_CHANNELS = Object.freeze([
|
|
4
|
+
'all', 'stereo', 'left', 'right',
|
|
5
|
+
'1', '2', '3', '4', '5', '6', '7', '8',
|
|
6
|
+
'34', '56', '78'
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
const COMMON_KEYS = new Set(['id', 'enabled', 'channel', 'assets', 'parameters']);
|
|
10
|
+
const CHANNEL_SET = new Set(EFFECT_CHANNELS);
|
|
11
|
+
const ASSET_NAMES_BY_EFFECT = new Map([
|
|
12
|
+
['FIRCrossover', ['impulseResponse']],
|
|
13
|
+
['FiveBandFIRPEQ', ['impulseResponse']],
|
|
14
|
+
['GroupDelayEQ', ['impulseResponse']],
|
|
15
|
+
['IRReverb', ['impulseResponse']],
|
|
16
|
+
['RoomEQ', ['impulseResponse']]
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateId(id) {
|
|
24
|
+
if (id === undefined) return undefined;
|
|
25
|
+
if (typeof id !== 'string' || id.length < 1 || id.length > 128) {
|
|
26
|
+
throw new ValidationError('Effect id must contain between 1 and 128 characters.');
|
|
27
|
+
}
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function validateChannel(channel = 'all') {
|
|
32
|
+
if (!CHANNEL_SET.has(channel)) {
|
|
33
|
+
throw new ValidationError(`Unsupported effect channel: ${String(channel)}`);
|
|
34
|
+
}
|
|
35
|
+
return channel;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateParameterValue(effectType, definition, value) {
|
|
39
|
+
const label = `${effectType}.${definition.name}`;
|
|
40
|
+
const values = definition.count > 1 ? value : [value];
|
|
41
|
+
if (definition.count > 1 &&
|
|
42
|
+
(!Array.isArray(value) || value.length !== definition.count)) {
|
|
43
|
+
throw new ValidationError(`${label} must contain exactly ${definition.count} values.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const item of values) {
|
|
47
|
+
if (definition.type === 'number' || definition.type === 'integer') {
|
|
48
|
+
if (typeof item !== 'number' || !Number.isFinite(item) ||
|
|
49
|
+
(definition.type === 'integer' && !Number.isInteger(item))) {
|
|
50
|
+
throw new ValidationError(`${label} must be ${definition.type === 'integer' ? 'an integer' : 'a finite number'}.`);
|
|
51
|
+
}
|
|
52
|
+
if (definition.minimum !== undefined && item < definition.minimum) {
|
|
53
|
+
throw new ValidationError(`${label} must be at least ${definition.minimum}.`);
|
|
54
|
+
}
|
|
55
|
+
if (definition.maximum !== undefined && item > definition.maximum) {
|
|
56
|
+
throw new ValidationError(`${label} must be at most ${definition.maximum}.`);
|
|
57
|
+
}
|
|
58
|
+
} else if (definition.type === 'string') {
|
|
59
|
+
if (typeof item !== 'string') {
|
|
60
|
+
throw new ValidationError(`${label} must be a string.`);
|
|
61
|
+
}
|
|
62
|
+
if (definition.maximumLength !== undefined && item.length > definition.maximumLength) {
|
|
63
|
+
throw new ValidationError(
|
|
64
|
+
`${label} must contain at most ${definition.maximumLength} characters.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (definition.pattern !== undefined) {
|
|
68
|
+
const match = new RegExp(definition.pattern, 'u').exec(item);
|
|
69
|
+
if (match?.[0] !== item) {
|
|
70
|
+
const example = typeof definition.default === 'string'
|
|
71
|
+
? ` (for example '${definition.default}')`
|
|
72
|
+
: '';
|
|
73
|
+
throw new ValidationError(
|
|
74
|
+
`${label} has an invalid format; expected a string matching ${definition.pattern}${example}.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else if (definition.type === 'boolean' && typeof item !== 'boolean') {
|
|
79
|
+
throw new ValidationError(`${label} must be a boolean.`);
|
|
80
|
+
}
|
|
81
|
+
if (definition.values && !definition.values.some(allowed => Object.is(allowed, item))) {
|
|
82
|
+
throw new ValidationError(`${label} has an unsupported value.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return Array.isArray(value) ? Object.freeze([...value]) : value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeBaseAssets(effectType, assets) {
|
|
89
|
+
const assetNames = ASSET_NAMES_BY_EFFECT.get(effectType);
|
|
90
|
+
if (!assetNames) {
|
|
91
|
+
if (assets !== undefined) throw new AssetError(`${effectType} does not accept external assets.`);
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
if (!isRecord(assets)) {
|
|
95
|
+
throw new AssetError(`${effectType} requires an assets object.`);
|
|
96
|
+
}
|
|
97
|
+
const allowed = new Set(assetNames);
|
|
98
|
+
for (const name of Object.keys(assets)) {
|
|
99
|
+
if (!allowed.has(name)) throw new AssetError(`${effectType} has no asset named ${name}.`);
|
|
100
|
+
}
|
|
101
|
+
const normalized = {};
|
|
102
|
+
for (const name of assetNames) {
|
|
103
|
+
const reference = assets[name];
|
|
104
|
+
if (typeof reference !== 'string' || reference.length < 1 || reference.length > 128) {
|
|
105
|
+
throw new AssetError(`${effectType}.${name} requires a non-empty asset reference.`);
|
|
106
|
+
}
|
|
107
|
+
normalized[name] = reference;
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze(normalized);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function collectParameters(effectType, options) {
|
|
113
|
+
const nested = options.parameters;
|
|
114
|
+
if (nested !== undefined && !isRecord(nested)) {
|
|
115
|
+
throw new ValidationError(`${effectType}.parameters must be an object.`);
|
|
116
|
+
}
|
|
117
|
+
if (nested !== undefined) {
|
|
118
|
+
for (const key of Object.keys(options)) {
|
|
119
|
+
if (!COMMON_KEYS.has(key)) {
|
|
120
|
+
throw new ValidationError(`${effectType} cannot mix parameters with top-level parameter options.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const supplied = nested ?? options;
|
|
125
|
+
const parameters = {};
|
|
126
|
+
for (const key of Object.keys(supplied)) {
|
|
127
|
+
if (nested === undefined && COMMON_KEYS.has(key)) continue;
|
|
128
|
+
const value = supplied[key];
|
|
129
|
+
parameters[key] = Array.isArray(value) ? Object.freeze([...value]) : value;
|
|
130
|
+
}
|
|
131
|
+
return Object.freeze(parameters);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class Effect {
|
|
135
|
+
constructor(type, options = {}) {
|
|
136
|
+
if (!isRecord(options)) throw new ValidationError(`${String(type)} options must be an object.`);
|
|
137
|
+
const id = validateId(options.id);
|
|
138
|
+
const enabled = options.enabled ?? true;
|
|
139
|
+
if (typeof enabled !== 'boolean') {
|
|
140
|
+
throw new ValidationError(`${type}.enabled must be boolean.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.type = type;
|
|
144
|
+
this.id = id;
|
|
145
|
+
this.enabled = enabled;
|
|
146
|
+
this.channel = validateChannel(options.channel ?? 'all');
|
|
147
|
+
this.parameters = collectParameters(type, options);
|
|
148
|
+
this.assets = normalizeBaseAssets(type, options.assets);
|
|
149
|
+
Object.freeze(this);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
toJSON() {
|
|
153
|
+
return {
|
|
154
|
+
...(this.id === undefined ? {} : { id: this.id }),
|
|
155
|
+
type: this.type,
|
|
156
|
+
enabled: this.enabled,
|
|
157
|
+
channel: this.channel,
|
|
158
|
+
parameters: Object.fromEntries(
|
|
159
|
+
Object.entries(this.parameters).map(([name, value]) => [
|
|
160
|
+
name,
|
|
161
|
+
Array.isArray(value) ? [...value] : value
|
|
162
|
+
])
|
|
163
|
+
),
|
|
164
|
+
...(this.assets === undefined ? {} : { assets: { ...this.assets } })
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
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 { EffeTuneError, EffeTuneRuntimeError } from './errors.js';
|
|
5
|
+
import { channelRange, packEffect } from './semantics.js';
|
|
6
|
+
import {
|
|
7
|
+
decodeTelemetryPacket,
|
|
8
|
+
TELEMETRY_RATE_HZ,
|
|
9
|
+
TELEMETRY_RING_BYTES
|
|
10
|
+
} from './telemetry.js';
|
|
11
|
+
|
|
12
|
+
function requireOk(status, operation) {
|
|
13
|
+
if (status !== ET_OK) {
|
|
14
|
+
throw new EffeTuneRuntimeError(`DSP ${operation} failed.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function createEngineSession(artifact, effects, resolvedAssets, {
|
|
19
|
+
sampleRate,
|
|
20
|
+
channels,
|
|
21
|
+
maxFrames,
|
|
22
|
+
seed
|
|
23
|
+
}) {
|
|
24
|
+
let binding;
|
|
25
|
+
const nodes = [];
|
|
26
|
+
try {
|
|
27
|
+
binding = await instantiateDspBinding(artifact.module ?? artifact.bytes ?? artifact, {
|
|
28
|
+
warning: () => {}
|
|
29
|
+
});
|
|
30
|
+
binding.createEngine();
|
|
31
|
+
requireOk(
|
|
32
|
+
binding.prepare(sampleRate, channels, maxFrames, TELEMETRY_RING_BYTES),
|
|
33
|
+
'preparation'
|
|
34
|
+
);
|
|
35
|
+
requireOk(binding.setTelemetryRate(0), 'telemetry configuration');
|
|
36
|
+
let tapId = 1;
|
|
37
|
+
for (const [effectIndex, effect] of effects.entries()) {
|
|
38
|
+
if (!effect.enabled) continue;
|
|
39
|
+
const packed = packEffect(effect);
|
|
40
|
+
const instanceId = binding.createInstance(packed.internalType);
|
|
41
|
+
if (!instanceId) throw new EffeTuneRuntimeError(`Unable to create ${effect.type}.`);
|
|
42
|
+
requireOk(binding.instanceSetTap(instanceId, tapId), `${effect.type} telemetry mapping`);
|
|
43
|
+
requireOk(binding.instanceSetSeed(instanceId, seed), `${effect.type} seed configuration`);
|
|
44
|
+
requireOk(
|
|
45
|
+
binding.instanceSetParams(instanceId, packed.values, packed.hash),
|
|
46
|
+
`${effect.type} parameter configuration`
|
|
47
|
+
);
|
|
48
|
+
if (packed.bytes) {
|
|
49
|
+
requireOk(
|
|
50
|
+
binding.instanceSetParamBytes(instanceId, packed.bytes, packed.hash),
|
|
51
|
+
`${effect.type} structured parameter configuration`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const implementation = getEffectImplementation(effect.type);
|
|
55
|
+
if (implementation.assets?.length) {
|
|
56
|
+
const prepared = prepareConvolutionAsset(effect, resolvedAssets, {
|
|
57
|
+
sampleRate,
|
|
58
|
+
engineChannels: channels
|
|
59
|
+
});
|
|
60
|
+
const slot = implementation.assets?.find(asset => asset.publicName === 'impulseResponse')?.slot ?? 0;
|
|
61
|
+
requireOk(
|
|
62
|
+
binding.instanceSetAsset(
|
|
63
|
+
instanceId,
|
|
64
|
+
slot,
|
|
65
|
+
prepared.payload,
|
|
66
|
+
prepared.beginInfo,
|
|
67
|
+
prepared.formatTag
|
|
68
|
+
),
|
|
69
|
+
`${effect.type} asset preparation`
|
|
70
|
+
);
|
|
71
|
+
const arena = binding.getArenaViews();
|
|
72
|
+
const warmupFrames = 128;
|
|
73
|
+
const processingChannels = prepared.beginInfo.processingChannels;
|
|
74
|
+
const silence = arena.scratch.allChannels.subarray(
|
|
75
|
+
0,
|
|
76
|
+
processingChannels * warmupFrames
|
|
77
|
+
);
|
|
78
|
+
const silencePtr = binding.pointerForArenaView(silence);
|
|
79
|
+
let state = binding.instanceAssetState(instanceId, slot);
|
|
80
|
+
const maximumWarmupBlocks = Math.ceil(2 * sampleRate / warmupFrames);
|
|
81
|
+
for (let block = 0; (state & 0xff) === 2 && block < maximumWarmupBlocks; block++) {
|
|
82
|
+
silence.fill(0);
|
|
83
|
+
requireOk(
|
|
84
|
+
binding.instanceProcess(
|
|
85
|
+
instanceId,
|
|
86
|
+
silencePtr,
|
|
87
|
+
processingChannels,
|
|
88
|
+
warmupFrames,
|
|
89
|
+
block * warmupFrames / sampleRate
|
|
90
|
+
),
|
|
91
|
+
`${effect.type} asset prewarming`
|
|
92
|
+
);
|
|
93
|
+
state = binding.instanceAssetState(instanceId, slot);
|
|
94
|
+
}
|
|
95
|
+
if ((state & 0xff) !== 3) {
|
|
96
|
+
throw new EffeTuneRuntimeError(`${effect.type} asset did not become active.`);
|
|
97
|
+
}
|
|
98
|
+
requireOk(binding.resetInstance(instanceId), `${effect.type} post-prewarm reset`);
|
|
99
|
+
requireOk(binding.instanceSetSeed(instanceId, seed), `${effect.type} post-prewarm seed reset`);
|
|
100
|
+
requireOk(
|
|
101
|
+
binding.instanceSetParams(instanceId, packed.values, packed.hash),
|
|
102
|
+
`${effect.type} post-prewarm parameter reset`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
nodes.push({
|
|
106
|
+
effectId: effect.id,
|
|
107
|
+
effectType: effect.type,
|
|
108
|
+
effectIndex,
|
|
109
|
+
instanceId,
|
|
110
|
+
tapId: tapId++,
|
|
111
|
+
range: channelRange(effect.channel, channels),
|
|
112
|
+
initialValues: new Float32Array(packed.values),
|
|
113
|
+
initialBytes: packed.bytes ? new Uint8Array(packed.bytes) : null,
|
|
114
|
+
hash: packed.hash
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return new EngineSession(binding, nodes, { channels, maxFrames, seed });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
binding?.close();
|
|
120
|
+
if (error instanceof EffeTuneError) throw error;
|
|
121
|
+
throw new EffeTuneRuntimeError('Unable to create the DSP processing state.', { cause: error });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class EngineSession {
|
|
126
|
+
constructor(binding, nodes, { channels, maxFrames, seed }) {
|
|
127
|
+
this.binding = binding;
|
|
128
|
+
this.nodes = nodes;
|
|
129
|
+
this.channels = channels;
|
|
130
|
+
this.maxFrames = maxFrames;
|
|
131
|
+
this.seed = seed;
|
|
132
|
+
this.closed = false;
|
|
133
|
+
this.arena = binding.getArenaViews().combined;
|
|
134
|
+
this.arenaByteOffset = this.arena.byteOffset;
|
|
135
|
+
this.fullChannelViews = Array.from({ length: channels }, (_, channel) =>
|
|
136
|
+
this.arena.subarray(channel * maxFrames, (channel + 1) * maxFrames)
|
|
137
|
+
);
|
|
138
|
+
this.nodesByTap = new Map(nodes.map(node => [node.tapId, node]));
|
|
139
|
+
this.telemetryBuffer = new Uint8Array(TELEMETRY_RING_BYTES);
|
|
140
|
+
this.telemetryCallbacks = new Set();
|
|
141
|
+
this.telemetryEnabled = false;
|
|
142
|
+
this.telemetryPendingDropped = 0;
|
|
143
|
+
this.droppedTelemetryFrames = 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get latencySamples() {
|
|
147
|
+
if (this.closed) throw new EffeTuneRuntimeError('DSP processing state is closed.');
|
|
148
|
+
let latency = 0;
|
|
149
|
+
for (const node of this.nodes) {
|
|
150
|
+
latency += this.binding.instanceLatency(node.instanceId);
|
|
151
|
+
}
|
|
152
|
+
return latency;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
process(input, output, offset, frameCount, sampleRate, timeFrame = offset) {
|
|
156
|
+
if (this.closed) throw new EffeTuneRuntimeError('DSP processing state is closed.');
|
|
157
|
+
for (let channel = 0; channel < this.channels; channel++) {
|
|
158
|
+
const target = frameCount === this.maxFrames
|
|
159
|
+
? this.fullChannelViews[channel]
|
|
160
|
+
: this.arena.subarray(channel * frameCount, (channel + 1) * frameCount);
|
|
161
|
+
target.set(offset === 0 && input[channel].length === frameCount
|
|
162
|
+
? input[channel]
|
|
163
|
+
: input[channel].subarray(offset, offset + frameCount));
|
|
164
|
+
}
|
|
165
|
+
for (const node of this.nodes) {
|
|
166
|
+
const audioPtr = this.arenaByteOffset +
|
|
167
|
+
node.range.start * frameCount * Float32Array.BYTES_PER_ELEMENT;
|
|
168
|
+
requireOk(
|
|
169
|
+
this.binding.instanceProcess(
|
|
170
|
+
node.instanceId,
|
|
171
|
+
audioPtr,
|
|
172
|
+
node.range.count,
|
|
173
|
+
frameCount,
|
|
174
|
+
timeFrame / sampleRate
|
|
175
|
+
),
|
|
176
|
+
`${node.effectType} processing`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
for (let channel = 0; channel < this.channels; channel++) {
|
|
180
|
+
const source = frameCount === this.maxFrames
|
|
181
|
+
? this.fullChannelViews[channel]
|
|
182
|
+
: this.arena.subarray(channel * frameCount, (channel + 1) * frameCount);
|
|
183
|
+
output[channel].set(source, offset);
|
|
184
|
+
}
|
|
185
|
+
if (this.telemetryCallbacks.size > 0) this.drainTelemetry();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
setTelemetryEnabled(enabled) {
|
|
189
|
+
if (this.closed) throw new EffeTuneRuntimeError('DSP processing state is closed.');
|
|
190
|
+
if (this.telemetryEnabled === enabled) return;
|
|
191
|
+
requireOk(
|
|
192
|
+
this.binding.setTelemetryRate(enabled ? TELEMETRY_RATE_HZ : 0),
|
|
193
|
+
'telemetry configuration'
|
|
194
|
+
);
|
|
195
|
+
this.telemetryEnabled = enabled;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
subscribe(callback) {
|
|
199
|
+
if (typeof callback !== 'function') {
|
|
200
|
+
throw new TypeError('Telemetry callback must be a function.');
|
|
201
|
+
}
|
|
202
|
+
this.telemetryCallbacks.add(callback);
|
|
203
|
+
if (this.telemetryCallbacks.size === 1) this.setTelemetryEnabled(true);
|
|
204
|
+
return () => this.unsubscribe(callback);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
unsubscribe(callback) {
|
|
208
|
+
const removed = this.telemetryCallbacks.delete(callback);
|
|
209
|
+
if (removed && this.telemetryCallbacks.size === 0) this.setTelemetryEnabled(false);
|
|
210
|
+
return removed;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
readTelemetryPacket(packet = this.telemetryBuffer) {
|
|
214
|
+
if (!(packet instanceof Uint8Array) || packet.byteLength < TELEMETRY_RING_BYTES) {
|
|
215
|
+
throw new TypeError(`Telemetry packet must hold at least ${TELEMETRY_RING_BYTES} bytes.`);
|
|
216
|
+
}
|
|
217
|
+
const bytes = this.binding.telemetryRead(packet);
|
|
218
|
+
const dropped = this.binding.lastTelemetryDroppedFrames;
|
|
219
|
+
this.droppedTelemetryFrames += dropped;
|
|
220
|
+
return { bytes, dropped };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
decodeTelemetry(packet, bytes, dropped = 0) {
|
|
224
|
+
const decoded = decodeTelemetryPacket(
|
|
225
|
+
packet,
|
|
226
|
+
bytes,
|
|
227
|
+
this.nodesByTap,
|
|
228
|
+
this.telemetryPendingDropped + dropped
|
|
229
|
+
);
|
|
230
|
+
this.telemetryPendingDropped = decoded.pendingDropped;
|
|
231
|
+
return decoded.frames;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
drainTelemetry() {
|
|
235
|
+
const { bytes, dropped } = this.readTelemetryPacket();
|
|
236
|
+
if (bytes === 0) {
|
|
237
|
+
this.telemetryPendingDropped += dropped;
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
const frames = this.decodeTelemetry(this.telemetryBuffer, bytes, dropped);
|
|
241
|
+
for (const frame of frames) {
|
|
242
|
+
for (const callback of this.telemetryCallbacks) {
|
|
243
|
+
try {
|
|
244
|
+
callback(frame);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.warn('EffeTune telemetry callback failed.', error);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return frames;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
setPacked(effectId, values, hash, bytes = null) {
|
|
254
|
+
const node = this.nodes.find(entry => entry.effectId === effectId);
|
|
255
|
+
if (!node) throw new EffeTuneRuntimeError(`Effect ${effectId} is not active.`);
|
|
256
|
+
requireOk(
|
|
257
|
+
this.binding.instanceSetParams(node.instanceId, values, hash),
|
|
258
|
+
`${node.effectType} parameter update`
|
|
259
|
+
);
|
|
260
|
+
if (bytes) {
|
|
261
|
+
requireOk(
|
|
262
|
+
this.binding.instanceSetParamBytes(node.instanceId, bytes, hash),
|
|
263
|
+
`${node.effectType} structured parameter update`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
reset() {
|
|
269
|
+
for (const node of this.nodes) {
|
|
270
|
+
requireOk(this.binding.resetInstance(node.instanceId), `${node.effectType} reset`);
|
|
271
|
+
requireOk(this.binding.instanceSetSeed(node.instanceId, this.seed), `${node.effectType} seed reset`);
|
|
272
|
+
requireOk(
|
|
273
|
+
this.binding.instanceSetParams(node.instanceId, node.initialValues, node.hash),
|
|
274
|
+
`${node.effectType} parameter reset`
|
|
275
|
+
);
|
|
276
|
+
if (node.initialBytes) {
|
|
277
|
+
requireOk(
|
|
278
|
+
this.binding.instanceSetParamBytes(node.instanceId, node.initialBytes, node.hash),
|
|
279
|
+
`${node.effectType} structured parameter reset`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
close() {
|
|
286
|
+
if (this.closed) return;
|
|
287
|
+
this.closed = true;
|
|
288
|
+
this.telemetryCallbacks.clear();
|
|
289
|
+
this.binding.close();
|
|
290
|
+
}
|
|
291
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class EffeTuneError extends Error {
|
|
2
|
+
constructor(message, options) {
|
|
3
|
+
super(message, options);
|
|
4
|
+
this.name = new.target.name;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ValidationError extends EffeTuneError {}
|
|
9
|
+
export class EffectError extends EffeTuneError {}
|
|
10
|
+
export class AssetError extends EffeTuneError {}
|
|
11
|
+
export class EffeTuneRuntimeError extends EffeTuneError {}
|
|
12
|
+
export class StateError extends EffeTuneError {}
|
|
13
|
+
|
|
14
|
+
const ERROR_TYPES = Object.freeze({
|
|
15
|
+
ValidationError,
|
|
16
|
+
EffectError,
|
|
17
|
+
AssetError,
|
|
18
|
+
EffeTuneRuntimeError,
|
|
19
|
+
StateError
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export function errorMessage(error, fallback) {
|
|
23
|
+
return error instanceof EffeTuneError
|
|
24
|
+
? { errorType: error.name, message: error.message }
|
|
25
|
+
: { errorType: 'EffeTuneRuntimeError', message: fallback };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function errorFromMessage(errorType, message, fallback) {
|
|
29
|
+
const ErrorType = ERROR_TYPES[errorType] ?? EffeTuneRuntimeError;
|
|
30
|
+
return new ErrorType(message || fallback);
|
|
31
|
+
}
|