EffeTune DSP high-level API design
Status: Phase 0 design conclusion, 2026-07-28
This document fixes the high-level product boundary needed to proceed to Phase 1. It is a design, not a released API or a promise of v1 compatibility. The Phase 0 runtime remains the narrow Compressor PoC.
Phase status
| Surface | Phase 0 status |
|---|---|
Python Compressor.process() over the existing C++ kernel |
Implemented in Phase 0 |
Real Chromium CompressorWorklet over baseline and SIMD WASM |
Implemented in Phase 0 |
Semantic Effect, Compressor, and ordered Chain API |
Designed in Phase 0 |
| Python offline/call/stream/preset API and JavaScript offline/worklet API | Designed in Phase 0 |
| Input layout, ownership, lifecycle, seed, errors, assets, preset boundaries, and telemetry boundary | Designed in Phase 0 |
Runtime Chain, streaming wrapper, preset importer, asset loading, full telemetry facade, and arbitrary kernels |
Deferred to Phase 1+ |
| File decode/encode, CLI, wheels/npm packages, bundles, automation, and full distribution support | Deferred to Phase 1+ |
Semantic model
Effect is the public semantic base type. An effect has a semantic type,
long-name parameters, an enabled flag, and optionally a caller-owned instance
ID. Internal registry names, generated parameter structs, packed fields,
layout hashes, and WASM handles are not properties of Effect.
Compressor is the only Phase 0 effect. Its six public parameters and defaults
are those in
chain-v1.schema.json. Phase 1 may add effect classes
without changing the shape of Effect or Chain.
Chain contains an ordered list of effects. Processing is strictly serial:
the output of entry n is the input of entry n + 1; disabled entries copy
through; an empty chain is the identity. Routing, buses, and parallel graphs
are not part of this chain contract.
The semantic document form is exactly:
{
"version": 1,
"chain": [
{
"id": "voice",
"type": "Compressor",
"enabled": true,
"parameters": {
"threshold": -24,
"ratio": 4
}
}
]
}
Schema default keywords are annotations. Loading wrappers materialize missing
semantic defaults before creating runtime state; validation alone never
modifies input. Duplicate explicit IDs are a validation error. For offline-only
documents an ID may be omitted; a wrapper may assign an instance ID, but
callers that need setParam use explicit IDs.
Python surface
The designed public shape is:
import effetune as et
compressor = et.Compressor(
threshold=-24,
ratio=4,
attack=5,
release=100,
knee=3,
gain=0,
)
chain = et.Chain([compressor])
output = chain.process(audio, sample_rate=48_000, seed=42)
same = chain(audio, 48_000, seed=42)
with chain.stream(
sample_rate=48_000,
channels=2,
block_size=512,
seed=42,
) as stream:
for block in input_blocks:
output_block = stream.process(block)
stream.reset()
chain = et.Chain.from_preset(
preset_document_or_path,
asset_resolver=resolver,
)
Effect.process(), Chain.process(), and Chain.__call__() are offline
operations. Each call creates fresh DSP state, so prior calls cannot affect the
result. __call__(audio, sample_rate, **options) is an exact convenience alias
for process(audio, sample_rate=sample_rate, **options).
Chain.stream() creates state once. Consecutive process() calls preserve
filter history, delay tails, and random-generator state. reset() restores the
same initial state and seed without replacing the stream. close() releases
native resources and is idempotent; processing or resetting after close raises
StateError. Context-manager exit always closes the stream. block_size is the
maximum accepted frame count, and a shorter final block is valid.
Python audio and ownership contract
- Audio is a C-contiguous NumPy
float32array shaped(channels, frames). - The sample rate is a positive finite value in Hz and is explicit on every offline call or stream creation.
- Offline and stream processing return a new C-contiguous array and do not mutate or retain the caller’s input.
- A stream’s channel count is fixed at creation. A block with another channel
count, a non-planar layout, another dtype, or more than
block_sizeframes is aValidationError. - File decoding and encoding are outside the DSP API. Applications provide
decoded planar float32 audio and choose
soundfile, ffmpeg, or another I/O layer themselves.
The current Phase 0 Compressor already implements the offline copy and input
layout behavior. Chain, __call__, stream(), reset(), close(), seed
handling, and preset loading are designs for Phase 1.
JavaScript and AudioWorklet surface
The designed offline API mirrors Python:
import { createChain } from '@effetune/dsp';
const chain = await createChain(chainDocument, { assetResolver });
const output = chain.process(planarFloat32, {
sampleRate: 48000,
seed: 42
});
chain.close();
planarFloat32 is an array of equal-length Float32Array channels. Processing
returns newly owned channel arrays and does not retain the inputs. Each
process() call has fresh DSP state. close() is idempotent and makes later
processing a StateError.
The designed real-time API owns one stateful chain inside one AudioWorklet:
import { EffeTuneNode } from '@effetune/dsp/worklet';
const node = await EffeTuneNode.create(audioContext, chainDocument, {
assetResolver,
seed: 42
});
source.connect(node).connect(audioContext.destination);
await node.setParam('voice', 'threshold', -20);
await node.reset();
node.close();
setParam(effectId, parameterName, value) accepts semantic long names,
validates the value, and resolves after the worklet acknowledges application
at its next safe processing boundary. It does not expose packed parameters or
sample-offset automation. The worklet retains DSP state until reset() or
close(). reset() restores the initial state and seed while keeping the
node usable. close() disconnects communication and releases wrapper-owned
resources; it is idempotent, and later mutation raises StateError.
The current Phase 0 CompressorWorklet proves WASM AudioWorklet execution only.
It does not implement EffeTuneNode, a runtime chain, parameter mutation,
reset, or the lifecycle contract above.
Determinism and seed
The public seed is an integer in the inclusive range 0 to 2^32 - 1.
Wrappers reject booleans, fractions, negative values, and larger integers.
The default seed is zero. With the same library/artifact version, semantic
chain, sample rate, channel layout, block schedule, seed, and input samples,
processing is deterministic. Wrappers may expand the public uint32 seed into
private kernel seed material; that representation is not public ABI.
Fresh offline calls restart from the public seed. Streams and worklets advance
state as they process and return to the initial seed on reset(). Cross-block
equivalence is guaranteed only for block schedules covered by the relevant
kernel contract and parity tests.
Presets, schema, legacy data, and assets
Chain.from_preset() accepts only the semantic chain document validated
against the matching schema version. A path argument means that the Python
wrapper reads UTF-8 JSON before validation; file I/O does not enter the DSP
core. The Phase 0
chain-v1.schema.json covers Compressor only.
The current EffeTune application preset forms (pipeline with long fields and
plugins with short fields) are separate formats. from_preset() must not
guess, coerce, or claim direct compatibility with them. A separately named
Phase 1 legacy importer converts a recognized application preset into a
canonical semantic chain document and reports unsupported routing, effects,
or fields. The semantic schema version selects migration logic; packed layout
hashes only check wrapper/artifact compatibility.
Effects that need external data receive semantic asset references from a future schema extension. The caller supplies an asset resolver:
def resolver(reference: et.AssetReference) -> et.AssetData | None:
...
The wrapper resolves and validates all required assets before preparing DSP
state. Missing, unreadable, wrong-kind, or malformed data raises AssetError
and identifies the semantic effect ID and asset reference. A wrapper never
silently bypasses an effect because an asset is missing. The concrete bundle
manifest and asset-reference JSON shape are Phase 1 work; they are
intentionally absent from the Compressor-only Phase 0 schema.
Error contract
Both language surfaces expose the same typed categories:
| Category | Meaning |
|---|---|
ValidationError |
Invalid document, audio layout, sample rate, seed, parameter, duplicate ID, or option |
EffectError |
Unknown, unavailable, or unsupported semantic effect type |
AssetError |
Required asset cannot be resolved or validated |
EffeTuneRuntimeError |
DSP preparation or processing failure, incompatible artifact, or internal runtime status |
StateError |
Operation is invalid for the wrapper lifecycle, such as processing after close |
Python exceptions derive from EffeTuneError; JavaScript error classes derive
from EffeTuneError. Raw numeric C ABI status values, stack traces, and
worklet message payloads remain diagnostics and are not the public error
contract.
Telemetry boundary
Telemetry is opt-in and wrapper-owned. Python offline/stream operations accept
an optional callback receiving decoded, named TelemetryFrame values.
EffeTuneNode provides the equivalent subscription and unsubscribe function.
Callbacks never run on the real-time DSP thread, and slow consumers may receive
a dropped-frame count. Audio processing does not fail merely because telemetry
is not consumed.
Raw binary telemetry frames, ring-buffer offsets, tap IDs, sequence layout, arena pointers, and AudioWorklet messages are private. The concrete set of public decoded telemetry record types is deferred until Phase 1; Phase 0 does not implement this facade.
Explicitly deferred
Phase 0 does not implement a runtime chain, preset migration, external assets, file/audio I/O, package bundles, all-kernel class generation, live automation, CLI commands, native shared-library distribution, wheels, npm publication, or installation guarantees. Those items require Phase 1 or later implementation and verification; this document only closes the Phase 0 API design gate.