@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026, Yoshiyuki Kobayashi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # @effetune/dsp
2
+
3
+ <!-- BEGIN DSP-LIBRARY-JAVASCRIPT-SUMMARY -->
4
+ EffeTune DSP provides the same MIT-licensed C++ audio kernels used by EffeTune
5
+ as a self-contained WebAssembly package for Node.js and evergreen browsers.
6
+ Version 0.1.0 exposes all 76 catalog types through the generic Chain and
7
+ `createEffect` APIs and 76 generated named convenience classes,
8
+ decoded analyzer telemetry, versioned semantic presets, deterministic seeds, and an AudioWorklet wrapper.
9
+ <!-- END DSP-LIBRARY-JAVASCRIPT-SUMMARY -->
10
+
11
+ Documentation: [effetune.frieve.com/dsp/](https://effetune.frieve.com/dsp/)
12
+
13
+ Source and issues:
14
+ [Frieve-A/effetune](https://github.com/Frieve-A/effetune)
15
+
16
+ <!-- BEGIN DSP-LIBRARY-JAVASCRIPT-START -->
17
+ ```console
18
+ npm install @effetune/dsp
19
+ ```
20
+
21
+ ```js
22
+ import { createChain } from '@effetune/dsp';
23
+
24
+ const frames = 512;
25
+ const mono = Float32Array.from(
26
+ { length: frames },
27
+ (_, frame) => 0.5 * Math.sin(2 * Math.PI * frame / 97)
28
+ );
29
+ const input = [mono.slice(), mono.slice()];
30
+ const chain = await createChain({
31
+ version: 1,
32
+ chain: [{
33
+ id: 'volume',
34
+ type: 'Volume',
35
+ parameters: { volume: -6 }
36
+ }]
37
+ });
38
+ const output = await chain.process(input, { sampleRate: 48000 });
39
+ console.log(output.length, output[0].length, output[0][0]);
40
+ chain.close();
41
+ ```
42
+ <!-- END DSP-LIBRARY-JAVASCRIPT-START -->
43
+
44
+ The package is ESM-only. Save the example as `start.mjs` and run
45
+ `node start.mjs`, or set `"type": "module"` in the consumer's `package.json`.
46
+ CommonJS `require()` is not supported.
47
+
48
+ Public package entry points are:
49
+
50
+ - `@effetune/dsp` for the main ESM API
51
+ - `@effetune/dsp/worklet` for `EffeTuneNode`
52
+ - `@effetune/dsp/processor` for the side-effect AudioWorklet processor
53
+ - `@effetune/dsp/schemas/chain-v1.json` for the Chain v1 JSON Schema
54
+ - `@effetune/dsp/schemas/bundle-v1.json` for the Bundle v1 JSON Schema
55
+ - `@effetune/dsp/catalog` for ESM catalog exports
56
+ - `@effetune/dsp/catalog.json` for the machine-readable catalog JSON
57
+
58
+ Every offline `process()` call starts from fresh DSP state and returns newly
59
+ owned `Float32Array` channels. Input arrays are never mutated. Effects run in
60
+ array order; a disabled effect and an empty chain are identity operations.
61
+ All samples must be finite. Offline and streaming calls reject `NaN`,
62
+ `Infinity`, and `-Infinity` with `ValidationError` before native processing;
63
+ an invalid stream block does not change filter state.
64
+
65
+ Use a stream when DSP state must continue across blocks or when parameters
66
+ change at an exact frame:
67
+
68
+ ```js
69
+ const stream = await chain.stream({
70
+ sampleRate: 48000,
71
+ channels: 2,
72
+ blockSize: 128,
73
+ seed: 42
74
+ });
75
+
76
+ const output = await stream.process([left, right], {
77
+ events: [{
78
+ frame: 256,
79
+ effectId: 'voice',
80
+ parameters: { threshold: -24, ratio: 4 }
81
+ }]
82
+ });
83
+
84
+ stream.setParam('voice', 'threshold', -20);
85
+ stream.reset();
86
+ stream.close();
87
+ ```
88
+
89
+ Event frames are relative to the start of that `process()` input. They must be
90
+ integers in input order from `0` through `frames - 1`; events sharing a frame
91
+ are applied in array order before that sample. Each parameter object is merged
92
+ with the effect's current semantic parameters and the complete packed block is
93
+ committed without recreating DSP state. `reset()` restores the parameters,
94
+ seed, and DSP state from stream creation.
95
+
96
+ `setParam()` and events cannot change parameters that require convolution
97
+ assets to be staged again. Open a new stream after changing
98
+ `IRReverb.channelMode`, `latency`, or `convolutionRate`;
99
+ `FIRCrossover.bandCount`, `latencyMode`, or `filterDelaySamples`; or
100
+ `latencyMode` / `filterDelaySamples` on `FiveBandFIRPEQ`, `GroupDelayEQ`, or
101
+ `RoomEQ`. The same restriction applies to `EffeTuneNode.setParam()`.
102
+
103
+ Canonical presets use semantic long parameter names:
104
+
105
+ ```json
106
+ {
107
+ "version": 1,
108
+ "chain": [
109
+ {
110
+ "id": "voice",
111
+ "type": "Compressor",
112
+ "enabled": true,
113
+ "channel": "stereo",
114
+ "parameters": { "threshold": -18, "ratio": 4 }
115
+ }
116
+ ]
117
+ }
118
+ ```
119
+
120
+ Application `pipeline` and `plugins` presets are not canonical presets.
121
+ Convert a preset whose effects form one ordered serial path explicitly with
122
+ `importLegacyPreset()` before calling `createChain()`. Branched and multi-bus
123
+ routing is rejected because flattening it would change the acoustic result.
124
+ Move or copy the desired effects into one serial path in the app and export it
125
+ again, or reproduce the branching in the host around separate Chains.
126
+ Unsupported fields produce a validation error.
127
+
128
+ FIR Crossover, Five Band FIR PEQ, Group Delay EQ, IR Reverb, and Room EQ
129
+ require an `assets.impulseResponse` reference and an `assetResolver`. The four
130
+ FIR filter effects use prepared coefficient impulses at the processing sample
131
+ rate. Use the public `encodeEta1({ channels, sampleRate, topology, paths })`
132
+ helper to encode raw planar float32 arrays for a resolver. Bundle manifests
133
+ verify exact byte length and SHA-256 before accepting an ETA1 payload. The
134
+ complete payload and convolution footprint must fit the 32 MiB kernel cap.
135
+
136
+ `EFFECT_CATALOG` and `getEffectCatalog()` expose the machine-readable semantic
137
+ catalog for all 76 root classes and their `create<Type>()` factories. The
138
+ catalog contains channel choices, parameters, required assets, telemetry, and
139
+ latency declarations, but no private implementation mapping.
140
+
141
+ For stateful processing, `ChainStream.latencySamples` reports aggregate runtime
142
+ latency and matches Python `Stream.latency_samples` for the same chain and
143
+ sample rate. `chain.latencySamples({ sampleRate })` reports the same value
144
+ without opening a stream, which aligns offline `process()` output.
145
+ `EffeTuneNode` does not expose a latency getter.
146
+
147
+ For real-time processing:
148
+
149
+ ```js
150
+ import { EffeTuneNode } from '@effetune/dsp/worklet';
151
+
152
+ const node = await EffeTuneNode.create(context, preset, {
153
+ channels: 2,
154
+ seed: 42,
155
+ assetResolver
156
+ });
157
+ source.connect(node).connect(context.destination);
158
+ await node.setParam('voice', 'threshold', -20);
159
+ await node.reset();
160
+ node.close();
161
+ ```
162
+
163
+ `LevelMeter`, `Oscilloscope`, `SpectrumAnalyzer`, `Spectrogram`, and
164
+ `StereoMeter` provide opt-in decoded telemetry:
165
+
166
+ ```js
167
+ const unsubscribe = node.subscribe(frame => {
168
+ if (frame.kind === 'level') console.log(frame.channels);
169
+ });
170
+ console.log(node.droppedTelemetryFrames);
171
+ unsubscribe();
172
+ ```
173
+
174
+ Offline `Chain.process()` accepts `onTelemetry`; a stream accepts the same
175
+ option and also provides `subscribe()`, `unsubscribe()`, and
176
+ `droppedTelemetryFrames`. The first subscriber enables observations and the
177
+ last unsubscribe disables them. Arrays in delivered frames belong to the
178
+ caller. Raw DSP telemetry and AudioWorklet messages are not public APIs.
179
+
180
+ `EffeTuneNode.create()` waits until the package-owned worklet processor has
181
+ instantiated the selected baseline or SIMD artifact and committed every
182
+ required asset. Serve the package files from the same origin or provide
183
+ explicit `processorUrl`, `wasmUrl`, and `simdWasmUrl` options.
184
+
185
+ The package does not decode or encode audio files. Provide planar float32 audio
186
+ from Web Audio, WebCodecs, an audio-file library, or your own I/O layer.
@@ -0,0 +1,11 @@
1
+ EffeTune DSP includes PFFFT v1.1.0.
2
+
3
+ PFFFT source: https://github.com/marton78/pffft
4
+
5
+ Copyright (c) 2020 Dario Mambro
6
+ Copyright (c) 2019 Hayati Ayguen
7
+ Copyright (c) 2013 Julien Pommier
8
+ Copyright (c) 2004 the University Corporation for Atmospheric Research
9
+
10
+ PFFFT is distributed under a permissive BSD-style license. The complete
11
+ upstream notice is included in dist/assets/NOTICE.txt.
@@ -0,0 +1,185 @@
1
+ import { detectSimdSupport } from './internal/dsp-wasm-loader.js';
2
+ import { instantiateDspBinding } from './internal/dsp-engine-binding.js';
3
+ import { getEffectCatalog, getEffectImplementation } from './catalog.js';
4
+ import { EffeTuneRuntimeError, ValidationError } from './errors.js';
5
+
6
+ const EXPECTED_ABI_VERSION = 1;
7
+ const cache = new Map();
8
+
9
+ function resourceUrl(value, fallback) {
10
+ if (value instanceof URL) return value;
11
+ if (value === undefined) return fallback;
12
+ try {
13
+ return new URL(String(value), import.meta.url);
14
+ } catch (error) {
15
+ throw new ValidationError(`Invalid DSP resource URL: ${String(value)}`, { cause: error });
16
+ }
17
+ }
18
+
19
+ async function readFileUrl(url, binary) {
20
+ try {
21
+ const { readFile } = await import('node:fs/promises');
22
+ const data = await readFile(url);
23
+ return binary
24
+ ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
25
+ : data.toString('utf8');
26
+ } catch (error) {
27
+ throw new EffeTuneRuntimeError(`Unable to read packaged DSP resource ${url.pathname}.`, { cause: error });
28
+ }
29
+ }
30
+
31
+ async function fetchResource(url, fetchImpl, binary) {
32
+ if (url.protocol === 'file:') return readFileUrl(url, binary);
33
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') {
34
+ throw new ValidationError(`Unsupported DSP resource URL scheme: ${url.protocol}`);
35
+ }
36
+ if (url.protocol === 'http:') {
37
+ const moduleUrl = new URL(import.meta.url);
38
+ const local = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
39
+ if (!local && url.origin !== moduleUrl.origin) {
40
+ throw new ValidationError('Cross-origin DSP resources must use HTTPS.');
41
+ }
42
+ }
43
+ if (typeof fetchImpl !== 'function') {
44
+ throw new EffeTuneRuntimeError('fetch is required to load DSP resources in this environment.');
45
+ }
46
+ let response;
47
+ try {
48
+ response = await fetchImpl(url);
49
+ } catch (error) {
50
+ throw new EffeTuneRuntimeError(`Unable to load DSP resource ${url}.`, { cause: error });
51
+ }
52
+ if (!response?.ok) {
53
+ throw new EffeTuneRuntimeError(`Unable to load DSP resource ${url} (HTTP ${response?.status ?? 'error'}).`);
54
+ }
55
+ try {
56
+ return await (binary ? response.arrayBuffer() : response.text());
57
+ } catch (error) {
58
+ throw new EffeTuneRuntimeError(`Unable to read DSP resource ${url}.`, { cause: error });
59
+ }
60
+ }
61
+
62
+ function selectVariant(requested, webAssembly) {
63
+ if (!['auto', 'baseline', 'simd'].includes(requested)) {
64
+ throw new ValidationError('variant must be auto, baseline, or simd.');
65
+ }
66
+ const supported = detectSimdSupport(webAssembly);
67
+ if (requested === 'simd' && !supported) {
68
+ throw new EffeTuneRuntimeError('This WebAssembly runtime does not support SIMD.');
69
+ }
70
+ return requested === 'auto' ? (supported ? 'simd' : 'baseline') : requested;
71
+ }
72
+
73
+ function validateMeta(meta) {
74
+ if (!meta || meta.abiVersion !== EXPECTED_ABI_VERSION || !Array.isArray(meta.kernels)) {
75
+ throw new EffeTuneRuntimeError('The packaged DSP metadata is incompatible.');
76
+ }
77
+ return new Map(meta.kernels.map(kernel => [kernel.name, kernel]));
78
+ }
79
+
80
+ function validateCapabilities(capabilities, kernels, variant) {
81
+ if (capabilities.abiVersion !== EXPECTED_ABI_VERSION) {
82
+ throw new EffeTuneRuntimeError('The packaged DSP module uses an incompatible ABI.');
83
+ }
84
+ if (capabilities.simd !== (variant === 'simd')) {
85
+ throw new EffeTuneRuntimeError(`The packaged ${variant} DSP artifact has inconsistent build flags.`);
86
+ }
87
+ const available = new Map(capabilities.kernels.map(kernel => [kernel.name, kernel]));
88
+ for (const effect of getEffectCatalog().effects) {
89
+ const implementation = getEffectImplementation(effect.type);
90
+ const meta = kernels.get(implementation.internalType);
91
+ const actual = available.get(implementation.internalType);
92
+ if (!meta || !actual ||
93
+ (meta.hash >>> 0) !== (implementation.layoutHash >>> 0) ||
94
+ (actual.hash >>> 0) !== (implementation.layoutHash >>> 0)) {
95
+ throw new EffeTuneRuntimeError(`${effect.type} is incompatible with the packaged DSP artifact.`);
96
+ }
97
+ }
98
+ }
99
+
100
+ async function loadUncached({
101
+ variant,
102
+ webAssembly,
103
+ fetchImpl,
104
+ wasmUrl,
105
+ simdWasmUrl,
106
+ metaUrl
107
+ }) {
108
+ if (!webAssembly || typeof webAssembly.compile !== 'function') {
109
+ throw new EffeTuneRuntimeError('WebAssembly is unavailable.');
110
+ }
111
+ const selected = selectVariant(variant, webAssembly);
112
+ const baselineDefault = new URL('./assets/effetune-dsp.wasm', import.meta.url);
113
+ const simdDefault = new URL('./assets/effetune-dsp.simd.wasm', import.meta.url);
114
+ const metadataDefault = new URL('./assets/effetune-dsp.meta.json', import.meta.url);
115
+ const selectedUrl = selected === 'simd'
116
+ ? resourceUrl(simdWasmUrl, simdDefault)
117
+ : resourceUrl(wasmUrl, baselineDefault);
118
+ const resolvedMetaUrl = resourceUrl(metaUrl, metadataDefault);
119
+
120
+ const [bytes, metaText] = await Promise.all([
121
+ fetchResource(selectedUrl, fetchImpl, true),
122
+ fetchResource(resolvedMetaUrl, fetchImpl, false)
123
+ ]);
124
+ let meta;
125
+ try {
126
+ meta = JSON.parse(metaText);
127
+ } catch (error) {
128
+ throw new EffeTuneRuntimeError('The packaged DSP metadata is not valid JSON.', { cause: error });
129
+ }
130
+ const kernels = validateMeta(meta);
131
+ let module;
132
+ try {
133
+ module = await webAssembly.compile(bytes);
134
+ } catch (error) {
135
+ throw new EffeTuneRuntimeError(`Unable to compile the packaged ${selected} DSP artifact.`, { cause: error });
136
+ }
137
+
138
+ let binding;
139
+ try {
140
+ binding = await instantiateDspBinding(module, { webAssembly, warning: () => {} });
141
+ validateCapabilities(binding.getCapabilities(), kernels, selected);
142
+ } catch (error) {
143
+ if (error instanceof EffeTuneRuntimeError) throw error;
144
+ throw new EffeTuneRuntimeError(`Unable to initialize the packaged ${selected} DSP artifact.`, { cause: error });
145
+ } finally {
146
+ binding?.close();
147
+ }
148
+ return { variant: selected, module, bytes, meta };
149
+ }
150
+
151
+ export async function loadDspArtifact(options = {}) {
152
+ const webAssembly = options.webAssembly ?? globalThis.WebAssembly;
153
+ const variant = selectVariant(options.variant ?? 'auto', webAssembly);
154
+ const wasmUrl = variant === 'simd' ? options.simdWasmUrl : options.wasmUrl;
155
+ const key = options.cache === false ||
156
+ options.fetch !== undefined ||
157
+ options.webAssembly !== undefined
158
+ ? null
159
+ : [
160
+ variant,
161
+ String(wasmUrl ?? ''),
162
+ String(options.metaUrl ?? ''),
163
+ options.fetch === undefined ? 'default-fetch' : 'custom-fetch'
164
+ ].join('|');
165
+ if (key && cache.has(key)) return cache.get(key);
166
+ const promise = loadUncached({
167
+ variant,
168
+ webAssembly,
169
+ fetchImpl: options.fetch ?? globalThis.fetch,
170
+ wasmUrl: options.wasmUrl,
171
+ simdWasmUrl: options.simdWasmUrl,
172
+ metaUrl: options.metaUrl
173
+ });
174
+ if (key) cache.set(key, promise);
175
+ try {
176
+ return await promise;
177
+ } catch (error) {
178
+ if (key) cache.delete(key);
179
+ throw error;
180
+ }
181
+ }
182
+
183
+ export function clearArtifactCache() {
184
+ cache.clear();
185
+ }
@@ -0,0 +1,30 @@
1
+ EffeTune DSP third-party notices
2
+ ================================
3
+
4
+ PFFFT v1.1.0
5
+ Source: https://github.com/marton78/pffft
6
+
7
+ Copyright (c) 2020 Dario Mambro (dario.mambro@gmail.com)
8
+ Copyright (c) 2019 Hayati Ayguen (h_ayguen@web.de)
9
+ Copyright (c) 2013 Julien Pommier (pommier@modartt.com)
10
+ Copyright (c) 2004 the University Corporation for Atmospheric Research (UCAR)
11
+
12
+ Redistribution and use of the Software in source and binary forms, with or without
13
+ modification, is permitted provided that the following conditions are met:
14
+
15
+ - Neither the names of NCAR's Computational and Information Systems Laboratory, the
16
+ University Corporation for Atmospheric Research, nor the names of its sponsors or
17
+ contributors may be used to endorse or promote products derived from this Software
18
+ without specific prior written permission.
19
+ - Redistributions of source code must retain the above copyright notices, this list of
20
+ conditions, and the disclaimer below.
21
+ - Redistributions in binary form must reproduce the above copyright notice, this list
22
+ of conditions, and the disclaimer below in the documentation and/or other materials
23
+ provided with the distribution.
24
+
25
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
26
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
27
+ PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE
28
+ LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29
+ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.