@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,175 @@
|
|
|
1
|
+
import { createEngineSession } from './engine.js';
|
|
2
|
+
import { errorMessage } from './errors.js';
|
|
3
|
+
import { countTelemetryFrames, TELEMETRY_RING_BYTES } from './telemetry.js';
|
|
4
|
+
|
|
5
|
+
class EffeTuneDspProcessor extends AudioWorkletProcessor {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.session = null;
|
|
9
|
+
this.ready = false;
|
|
10
|
+
this.closed = false;
|
|
11
|
+
this.channels = 0;
|
|
12
|
+
this.pendingCommands = [];
|
|
13
|
+
this.sourceChannels = [];
|
|
14
|
+
this.targetChannels = [];
|
|
15
|
+
this.telemetryEnabled = false;
|
|
16
|
+
this.telemetryPackets = Array.from(
|
|
17
|
+
{ length: 4 },
|
|
18
|
+
() => new ArrayBuffer(TELEMETRY_RING_BYTES)
|
|
19
|
+
);
|
|
20
|
+
this.telemetryDiscardPacket = new Uint8Array(TELEMETRY_RING_BYTES);
|
|
21
|
+
this.telemetryDropped = 0;
|
|
22
|
+
this.port.onmessage = event => this.handleMessage(event.data);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
handleMessage(message) {
|
|
26
|
+
if (message?.type === 'initialize') {
|
|
27
|
+
this.initialize(message);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (message?.type === 'close') {
|
|
31
|
+
this.closed = true;
|
|
32
|
+
this.session?.close();
|
|
33
|
+
this.session = null;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (message?.type === 'telemetryReturn') {
|
|
37
|
+
if (message.packet instanceof ArrayBuffer &&
|
|
38
|
+
message.packet.byteLength === TELEMETRY_RING_BYTES) {
|
|
39
|
+
this.telemetryPackets.push(message.packet);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (message?.type === 'setParam' || message?.type === 'reset' ||
|
|
44
|
+
message?.type === 'setTelemetryEnabled') {
|
|
45
|
+
this.pendingCommands.push(message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async initialize(message) {
|
|
50
|
+
try {
|
|
51
|
+
this.channels = message.channels;
|
|
52
|
+
this.sourceChannels = new Array(this.channels);
|
|
53
|
+
this.targetChannels = new Array(this.channels);
|
|
54
|
+
const active = message.document.chain.some(effect => effect.enabled);
|
|
55
|
+
if (active) {
|
|
56
|
+
this.session = await createEngineSession(
|
|
57
|
+
{ bytes: message.wasmBytes },
|
|
58
|
+
message.document.chain,
|
|
59
|
+
message.resolvedAssets,
|
|
60
|
+
{
|
|
61
|
+
sampleRate,
|
|
62
|
+
channels: message.channels,
|
|
63
|
+
maxFrames: 128,
|
|
64
|
+
seed: message.seed
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
this.ready = true;
|
|
69
|
+
this.port.postMessage({ type: 'ready' });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.port.postMessage({
|
|
72
|
+
type: 'initializationError',
|
|
73
|
+
...errorMessage(
|
|
74
|
+
error,
|
|
75
|
+
'The AudioWorklet DSP processor could not be initialized.'
|
|
76
|
+
)
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
applyCommands() {
|
|
82
|
+
for (const command of this.pendingCommands.splice(0)) {
|
|
83
|
+
try {
|
|
84
|
+
if (command.type === 'setParam') {
|
|
85
|
+
this.session?.setPacked(
|
|
86
|
+
command.effectId,
|
|
87
|
+
command.values,
|
|
88
|
+
command.hash,
|
|
89
|
+
command.bytes
|
|
90
|
+
);
|
|
91
|
+
} else if (command.type === 'reset') {
|
|
92
|
+
this.session?.reset();
|
|
93
|
+
} else if (command.type === 'setTelemetryEnabled') {
|
|
94
|
+
this.telemetryEnabled = command.enabled === true;
|
|
95
|
+
this.session?.setTelemetryEnabled(this.telemetryEnabled);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
this.port.postMessage({ type: 'commandResult', commandId: command.commandId, ok: true });
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.port.postMessage({
|
|
101
|
+
type: 'commandResult',
|
|
102
|
+
commandId: command.commandId,
|
|
103
|
+
ok: false,
|
|
104
|
+
...errorMessage(error, 'The AudioWorklet command failed.')
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
drainTelemetry() {
|
|
111
|
+
if (!this.session || !this.telemetryEnabled) return;
|
|
112
|
+
const buffer = this.telemetryPackets.pop();
|
|
113
|
+
const packet = buffer ? new Uint8Array(buffer) : this.telemetryDiscardPacket;
|
|
114
|
+
const { bytes, dropped } = this.session.readTelemetryPacket(packet);
|
|
115
|
+
if (!buffer) {
|
|
116
|
+
this.telemetryDropped += dropped + countTelemetryFrames(packet, bytes);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (bytes === 0) {
|
|
120
|
+
this.telemetryDropped += dropped;
|
|
121
|
+
this.telemetryPackets.push(buffer);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.port.postMessage({
|
|
125
|
+
type: 'telemetry',
|
|
126
|
+
packet: buffer,
|
|
127
|
+
bytes,
|
|
128
|
+
dropped: this.telemetryDropped + dropped
|
|
129
|
+
}, [buffer]);
|
|
130
|
+
this.telemetryDropped = 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
passthrough(input, output) {
|
|
134
|
+
for (let channel = 0; channel < output.length; channel++) {
|
|
135
|
+
const source = input[channel];
|
|
136
|
+
if (source) output[channel].set(source);
|
|
137
|
+
else output[channel].fill(0);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
process(inputs, outputs) {
|
|
142
|
+
if (this.closed) return false;
|
|
143
|
+
const input = inputs[0] ?? [];
|
|
144
|
+
const output = outputs[0] ?? [];
|
|
145
|
+
if (!this.ready) {
|
|
146
|
+
this.passthrough(input, output);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
this.applyCommands();
|
|
150
|
+
if (!this.session) {
|
|
151
|
+
this.passthrough(input, output);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
for (let channel = 0; channel < this.channels; channel++) {
|
|
155
|
+
this.sourceChannels[channel] = input[channel] ?? output[channel];
|
|
156
|
+
this.targetChannels[channel] = output[channel];
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
this.session.process(this.sourceChannels, this.targetChannels, 0, 128, sampleRate);
|
|
160
|
+
this.drainTelemetry();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
this.session.close();
|
|
163
|
+
this.session = null;
|
|
164
|
+
for (const channel of output) channel.fill(0);
|
|
165
|
+
this.port.postMessage({
|
|
166
|
+
type: 'processingError',
|
|
167
|
+
...errorMessage(error, 'Audio processing failed.')
|
|
168
|
+
});
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
registerProcessor('effetune-dsp-processor', EffeTuneDspProcessor);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ArtifactOptions,
|
|
3
|
+
AssetResolver,
|
|
4
|
+
BundleDocument,
|
|
5
|
+
ChainDocumentInput,
|
|
6
|
+
ChainEffectInput,
|
|
7
|
+
TelemetryCallback
|
|
8
|
+
} from './index.js';
|
|
9
|
+
import type { Effect } from './effect.js';
|
|
10
|
+
|
|
11
|
+
export interface EffeTuneNodeOptions extends ArtifactOptions {
|
|
12
|
+
readonly channels?: number;
|
|
13
|
+
readonly seed?: number;
|
|
14
|
+
readonly assetResolver?: AssetResolver | { resolve: AssetResolver };
|
|
15
|
+
readonly processorUrl?: string | URL;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export declare class EffeTuneNode extends AudioWorkletNode {
|
|
19
|
+
private constructor();
|
|
20
|
+
static create(
|
|
21
|
+
context: BaseAudioContext,
|
|
22
|
+
input: string | ChainDocumentInput | BundleDocument |
|
|
23
|
+
readonly (Effect | ChainEffectInput)[],
|
|
24
|
+
options?: EffeTuneNodeOptions
|
|
25
|
+
): Promise<EffeTuneNode>;
|
|
26
|
+
readonly droppedTelemetryFrames: number;
|
|
27
|
+
subscribe(callback: TelemetryCallback): () => void;
|
|
28
|
+
unsubscribe(callback: TelemetryCallback): boolean;
|
|
29
|
+
setParam(effectId: string, parameterName: string, value: unknown): Promise<void>;
|
|
30
|
+
reset(): Promise<void>;
|
|
31
|
+
close(): void;
|
|
32
|
+
}
|
package/dist/worklet.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { loadDspArtifact } from './artifacts.js';
|
|
2
|
+
import { resolveChainAssets, splitBundle } from './assets.js';
|
|
3
|
+
import {
|
|
4
|
+
EffeTuneRuntimeError,
|
|
5
|
+
errorFromMessage,
|
|
6
|
+
StateError,
|
|
7
|
+
ValidationError
|
|
8
|
+
} from './errors.js';
|
|
9
|
+
import {
|
|
10
|
+
normalizeChainDocument,
|
|
11
|
+
channelRange,
|
|
12
|
+
packEffect,
|
|
13
|
+
setEffectParameter,
|
|
14
|
+
validateStreamParameterUpdate,
|
|
15
|
+
validateEffectSampleRate,
|
|
16
|
+
validateSeed
|
|
17
|
+
} from './semantics.js';
|
|
18
|
+
import { decodeTelemetryPacket } from './telemetry.js';
|
|
19
|
+
|
|
20
|
+
const PROCESSOR_NAME = 'effetune-dsp-processor';
|
|
21
|
+
const AudioWorkletNodeBase = globalThis.AudioWorkletNode ?? class {};
|
|
22
|
+
const moduleLoads = new WeakMap();
|
|
23
|
+
|
|
24
|
+
function activeEffects(document) {
|
|
25
|
+
return document.chain.filter(effect => effect.enabled);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cloneDocument(document) {
|
|
29
|
+
return {
|
|
30
|
+
version: 1,
|
|
31
|
+
chain: document.chain.map(effect => ({
|
|
32
|
+
...effect,
|
|
33
|
+
parameters: Object.fromEntries(
|
|
34
|
+
Object.entries(effect.parameters).map(([name, value]) => [
|
|
35
|
+
name,
|
|
36
|
+
Array.isArray(value) ? [...value] : value
|
|
37
|
+
])
|
|
38
|
+
),
|
|
39
|
+
...(effect.assets ? { assets: { ...effect.assets } } : {})
|
|
40
|
+
}))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateChannels(channels) {
|
|
45
|
+
if (!Number.isInteger(channels) || channels < 1 || channels > 8) {
|
|
46
|
+
throw new ValidationError('channels must be an integer from 1 to 8.');
|
|
47
|
+
}
|
|
48
|
+
return channels;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadProcessorModule(context, processorUrl) {
|
|
52
|
+
if (!context?.audioWorklet || typeof context.audioWorklet.addModule !== 'function') {
|
|
53
|
+
throw new ValidationError('An AudioContext with AudioWorklet support is required.');
|
|
54
|
+
}
|
|
55
|
+
let byUrl = moduleLoads.get(context.audioWorklet);
|
|
56
|
+
if (!byUrl) {
|
|
57
|
+
byUrl = new Map();
|
|
58
|
+
moduleLoads.set(context.audioWorklet, byUrl);
|
|
59
|
+
}
|
|
60
|
+
const key = String(processorUrl);
|
|
61
|
+
if (!byUrl.has(key)) {
|
|
62
|
+
let load;
|
|
63
|
+
try {
|
|
64
|
+
load = Promise.resolve(context.audioWorklet.addModule(processorUrl));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new EffeTuneRuntimeError(
|
|
67
|
+
'Unable to load the AudioWorklet DSP processor module.',
|
|
68
|
+
{ cause: error }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const cached = load.catch(error => {
|
|
72
|
+
if (byUrl.get(key) === cached) byUrl.delete(key);
|
|
73
|
+
throw new EffeTuneRuntimeError(
|
|
74
|
+
'Unable to load the AudioWorklet DSP processor module.',
|
|
75
|
+
{ cause: error }
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
byUrl.set(key, cached);
|
|
79
|
+
}
|
|
80
|
+
return byUrl.get(key);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function validateProcessorUrl(value) {
|
|
84
|
+
const url = value instanceof URL ? value : new URL(String(value), import.meta.url);
|
|
85
|
+
if (!['file:', 'https:', 'http:'].includes(url.protocol)) {
|
|
86
|
+
throw new ValidationError(`Unsupported AudioWorklet processor URL scheme: ${url.protocol}`);
|
|
87
|
+
}
|
|
88
|
+
if (url.protocol === 'http:') {
|
|
89
|
+
const moduleUrl = new URL(import.meta.url);
|
|
90
|
+
const local = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
91
|
+
if (!local && url.origin !== moduleUrl.origin) {
|
|
92
|
+
throw new ValidationError('Cross-origin AudioWorklet processors must use HTTPS.');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return url;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function cloneAssetsForWorklet(resolvedAssets) {
|
|
99
|
+
const cloned = new Map();
|
|
100
|
+
const transfer = [];
|
|
101
|
+
for (const [effectId, assets] of resolvedAssets) {
|
|
102
|
+
const values = {};
|
|
103
|
+
for (const [name, asset] of Object.entries(assets)) {
|
|
104
|
+
const bytes = new Uint8Array(asset.bytes);
|
|
105
|
+
values[name] = { ...asset, bytes };
|
|
106
|
+
transfer.push(bytes.buffer);
|
|
107
|
+
}
|
|
108
|
+
cloned.set(effectId, values);
|
|
109
|
+
}
|
|
110
|
+
return { cloned, transfer };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class EffeTuneNode extends AudioWorkletNodeBase {
|
|
114
|
+
static async create(context, input, options = {}) {
|
|
115
|
+
const channels = validateChannels(options.channels ?? 2);
|
|
116
|
+
const seed = validateSeed(options.seed ?? 0);
|
|
117
|
+
const source = typeof input === 'string'
|
|
118
|
+
? (() => {
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(input);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw new ValidationError('Chain input is not valid JSON.', { cause: error });
|
|
123
|
+
}
|
|
124
|
+
})()
|
|
125
|
+
: input;
|
|
126
|
+
const { chain, manifest } = splitBundle(source);
|
|
127
|
+
const document = normalizeChainDocument(chain);
|
|
128
|
+
const resolvedAssets = await resolveChainAssets(document, {
|
|
129
|
+
assetResolver: options.assetResolver,
|
|
130
|
+
manifest
|
|
131
|
+
});
|
|
132
|
+
for (const effect of activeEffects(document)) {
|
|
133
|
+
validateEffectSampleRate(effect, context.sampleRate);
|
|
134
|
+
channelRange(effect.channel, channels);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const processorUrl = validateProcessorUrl(
|
|
138
|
+
options.processorUrl ?? new URL('./worklet-processor.js', import.meta.url)
|
|
139
|
+
);
|
|
140
|
+
await loadProcessorModule(context, processorUrl);
|
|
141
|
+
const artifact = activeEffects(document).length > 0
|
|
142
|
+
? await loadDspArtifact({
|
|
143
|
+
variant: options.variant,
|
|
144
|
+
wasmUrl: options.wasmUrl,
|
|
145
|
+
simdWasmUrl: options.simdWasmUrl,
|
|
146
|
+
metaUrl: options.metaUrl,
|
|
147
|
+
fetch: options.fetch,
|
|
148
|
+
webAssembly: options.webAssembly,
|
|
149
|
+
cache: options.cache
|
|
150
|
+
})
|
|
151
|
+
: null;
|
|
152
|
+
const node = new EffeTuneNode(context, channels, document, seed);
|
|
153
|
+
const wasmBytes = artifact ? artifact.bytes.slice(0) : null;
|
|
154
|
+
const assets = cloneAssetsForWorklet(resolvedAssets);
|
|
155
|
+
const transfer = [...assets.transfer, ...(wasmBytes ? [wasmBytes] : [])];
|
|
156
|
+
node.port.postMessage({
|
|
157
|
+
type: 'initialize',
|
|
158
|
+
document,
|
|
159
|
+
resolvedAssets: assets.cloned,
|
|
160
|
+
wasmBytes,
|
|
161
|
+
channels,
|
|
162
|
+
seed
|
|
163
|
+
}, transfer);
|
|
164
|
+
try {
|
|
165
|
+
await node._waitUntilReady();
|
|
166
|
+
return node;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
node.close();
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
constructor(context, channels, document, seed) {
|
|
174
|
+
super(context, PROCESSOR_NAME, {
|
|
175
|
+
numberOfInputs: 1,
|
|
176
|
+
numberOfOutputs: 1,
|
|
177
|
+
outputChannelCount: [channels],
|
|
178
|
+
channelCount: channels,
|
|
179
|
+
channelCountMode: 'explicit',
|
|
180
|
+
channelInterpretation: 'discrete'
|
|
181
|
+
});
|
|
182
|
+
this._initialDocument = cloneDocument(document);
|
|
183
|
+
this._document = cloneDocument(document);
|
|
184
|
+
this._seed = seed;
|
|
185
|
+
this._closed = false;
|
|
186
|
+
this._runtimeError = null;
|
|
187
|
+
this._nextCommandId = 1;
|
|
188
|
+
this._pending = new Map();
|
|
189
|
+
this._telemetryCallbacks = new Set();
|
|
190
|
+
this._telemetryPendingDropped = 0;
|
|
191
|
+
this._droppedTelemetryFrames = 0;
|
|
192
|
+
let tapId = 1;
|
|
193
|
+
this._telemetryNodesByTap = new Map();
|
|
194
|
+
for (const [effectIndex, effect] of document.chain.entries()) {
|
|
195
|
+
if (!effect.enabled) continue;
|
|
196
|
+
this._telemetryNodesByTap.set(tapId++, {
|
|
197
|
+
effectType: effect.type,
|
|
198
|
+
effectId: effect.id,
|
|
199
|
+
effectIndex
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
this._mutationQueue = Promise.resolve();
|
|
203
|
+
this._ready = new Promise((resolve, reject) => {
|
|
204
|
+
this._resolveReady = resolve;
|
|
205
|
+
this._rejectReady = reject;
|
|
206
|
+
});
|
|
207
|
+
this.port.onmessage = event => this._handleMessage(event.data);
|
|
208
|
+
this.port.start?.();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_handleMessage(message) {
|
|
212
|
+
if (message?.type === 'telemetry') {
|
|
213
|
+
const packet = message.packet;
|
|
214
|
+
const dropped = Number.isInteger(message.dropped) && message.dropped >= 0
|
|
215
|
+
? message.dropped
|
|
216
|
+
: 0;
|
|
217
|
+
this._droppedTelemetryFrames += dropped;
|
|
218
|
+
if (packet instanceof ArrayBuffer) {
|
|
219
|
+
const decoded = decodeTelemetryPacket(
|
|
220
|
+
new Uint8Array(packet),
|
|
221
|
+
message.bytes,
|
|
222
|
+
this._telemetryNodesByTap,
|
|
223
|
+
this._telemetryPendingDropped + dropped
|
|
224
|
+
);
|
|
225
|
+
this._telemetryPendingDropped = decoded.pendingDropped;
|
|
226
|
+
for (const frame of decoded.frames) {
|
|
227
|
+
for (const callback of this._telemetryCallbacks) {
|
|
228
|
+
try {
|
|
229
|
+
callback(frame);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.warn('EffeTune telemetry callback failed.', error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
this.port.postMessage({ type: 'telemetryReturn', packet }, [packet]);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (message?.type === 'ready') {
|
|
240
|
+
this._resolveReady();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (message?.type === 'initializationError') {
|
|
244
|
+
this._rejectReady(errorFromMessage(
|
|
245
|
+
message.errorType,
|
|
246
|
+
message.message,
|
|
247
|
+
'The AudioWorklet DSP processor could not be initialized.'
|
|
248
|
+
));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (message?.type === 'processingError') {
|
|
252
|
+
const error = errorFromMessage(
|
|
253
|
+
message.errorType,
|
|
254
|
+
message.message,
|
|
255
|
+
'The AudioWorklet DSP processor stopped after a processing error.'
|
|
256
|
+
);
|
|
257
|
+
this._runtimeError = error;
|
|
258
|
+
for (const pending of this._pending.values()) pending.reject(error);
|
|
259
|
+
this._pending.clear();
|
|
260
|
+
this.dispatchEvent?.(new Event('processorerror'));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (message?.type !== 'commandResult') return;
|
|
264
|
+
const pending = this._pending.get(message.commandId);
|
|
265
|
+
if (!pending) return;
|
|
266
|
+
this._pending.delete(message.commandId);
|
|
267
|
+
if (message.ok) pending.resolve();
|
|
268
|
+
else pending.reject(errorFromMessage(
|
|
269
|
+
message.errorType,
|
|
270
|
+
message.message,
|
|
271
|
+
'The AudioWorklet command failed.'
|
|
272
|
+
));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async _waitUntilReady() {
|
|
276
|
+
const timeout = new Promise((_, reject) => {
|
|
277
|
+
const timer = setTimeout(() => {
|
|
278
|
+
reject(errorFromMessage(
|
|
279
|
+
'EffeTuneRuntimeError',
|
|
280
|
+
undefined,
|
|
281
|
+
'Timed out while initializing the AudioWorklet DSP processor.'
|
|
282
|
+
));
|
|
283
|
+
}, 10000);
|
|
284
|
+
this._ready.then(
|
|
285
|
+
() => clearTimeout(timer),
|
|
286
|
+
() => clearTimeout(timer)
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
return Promise.race([this._ready, timeout]);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_assertOpen() {
|
|
293
|
+
if (this._closed) throw new StateError('The EffeTuneNode is closed.');
|
|
294
|
+
if (this._runtimeError) throw this._runtimeError;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_command(command) {
|
|
298
|
+
this._assertOpen();
|
|
299
|
+
const commandId = this._nextCommandId++;
|
|
300
|
+
const promise = new Promise((resolve, reject) => {
|
|
301
|
+
this._pending.set(commandId, { resolve, reject });
|
|
302
|
+
});
|
|
303
|
+
this.port.postMessage({ ...command, commandId });
|
|
304
|
+
return promise;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
_mutate(operation) {
|
|
308
|
+
const result = this._mutationQueue.then(operation);
|
|
309
|
+
this._mutationQueue = result.catch(() => {});
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
get droppedTelemetryFrames() {
|
|
314
|
+
this._assertOpen();
|
|
315
|
+
return this._droppedTelemetryFrames;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
subscribe(callback) {
|
|
319
|
+
this._assertOpen();
|
|
320
|
+
if (typeof callback !== 'function') {
|
|
321
|
+
throw new TypeError('Telemetry callback must be a function.');
|
|
322
|
+
}
|
|
323
|
+
const wasEmpty = this._telemetryCallbacks.size === 0;
|
|
324
|
+
this._telemetryCallbacks.add(callback);
|
|
325
|
+
if (wasEmpty) {
|
|
326
|
+
this.port.postMessage({ type: 'setTelemetryEnabled', enabled: true });
|
|
327
|
+
}
|
|
328
|
+
return () => this.unsubscribe(callback);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
unsubscribe(callback) {
|
|
332
|
+
this._assertOpen();
|
|
333
|
+
const removed = this._telemetryCallbacks.delete(callback);
|
|
334
|
+
if (removed && this._telemetryCallbacks.size === 0) {
|
|
335
|
+
this.port.postMessage({ type: 'setTelemetryEnabled', enabled: false });
|
|
336
|
+
}
|
|
337
|
+
return removed;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
setParam(effectId, parameterName, value) {
|
|
341
|
+
return this._mutate(async () => {
|
|
342
|
+
this._assertOpen();
|
|
343
|
+
const index = this._document.chain.findIndex(effect => effect.id === effectId);
|
|
344
|
+
if (index < 0) throw new ValidationError(`Unknown effect id: ${effectId}`);
|
|
345
|
+
const effects = [...this._document.chain];
|
|
346
|
+
validateStreamParameterUpdate(effects[index], parameterName);
|
|
347
|
+
effects[index] = setEffectParameter(effects[index], parameterName, value);
|
|
348
|
+
if (effects[index].enabled) {
|
|
349
|
+
const packed = packEffect(effects[index]);
|
|
350
|
+
await this._command({
|
|
351
|
+
type: 'setParam',
|
|
352
|
+
effectId,
|
|
353
|
+
values: packed.values,
|
|
354
|
+
hash: packed.hash,
|
|
355
|
+
bytes: packed.bytes
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
this._document = { version: 1, chain: effects };
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
reset() {
|
|
363
|
+
return this._mutate(async () => {
|
|
364
|
+
this._assertOpen();
|
|
365
|
+
await this._command({ type: 'reset' });
|
|
366
|
+
this._document = cloneDocument(this._initialDocument);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
close() {
|
|
371
|
+
if (this._closed) return;
|
|
372
|
+
this._closed = true;
|
|
373
|
+
this._telemetryCallbacks.clear();
|
|
374
|
+
this.port.postMessage({ type: 'close' });
|
|
375
|
+
for (const pending of this._pending.values()) {
|
|
376
|
+
pending.reject(new StateError('The EffeTuneNode was closed before its command completed.'));
|
|
377
|
+
}
|
|
378
|
+
this._pending.clear();
|
|
379
|
+
this.port.onmessage = null;
|
|
380
|
+
try {
|
|
381
|
+
this.disconnect();
|
|
382
|
+
} catch {
|
|
383
|
+
// Already disconnected.
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effetune/dsp",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "EffeTune's deterministic WebAssembly audio effects for JavaScript and AudioWorklet",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Yoshiyuki Kobayashi",
|
|
8
|
+
"homepage": "https://effetune.frieve.com/dsp/",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/Frieve-A/effetune",
|
|
12
|
+
"directory": "dsp/bindings/js"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Frieve-A/effetune/issues"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"./dist/worklet-processor.js"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"README.md",
|
|
27
|
+
"THIRD_PARTY_NOTICES.txt"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./worklet": {
|
|
35
|
+
"types": "./dist/worklet.d.ts",
|
|
36
|
+
"import": "./dist/worklet.js"
|
|
37
|
+
},
|
|
38
|
+
"./processor": "./dist/worklet-processor.js",
|
|
39
|
+
"./schemas/chain-v1.json": "./dist/schemas/chain-v1.schema.json",
|
|
40
|
+
"./schemas/bundle-v1.json": "./dist/schemas/bundle-v1.schema.json",
|
|
41
|
+
"./catalog": {
|
|
42
|
+
"types": "./dist/catalog-entry.d.ts",
|
|
43
|
+
"import": "./dist/catalog-entry.js"
|
|
44
|
+
},
|
|
45
|
+
"./catalog.json": "./dist/catalog/effects-v1.json"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "node scripts/build.mjs",
|
|
49
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
50
|
+
"pack:check": "node scripts/verify-pack.mjs",
|
|
51
|
+
"install:check": "node scripts/verify-install.mjs"
|
|
52
|
+
}
|
|
53
|
+
}
|