Custom Pre- and Post-Processing¶
When the default PrePostProcessor doesn’t cover what your
model needs — windowing, normalization, parameter smoothing, custom
multi-tensor packing — you subclass JSPrePostProcessor and
override preProcess and/or postProcess in JavaScript. The
guitar-lstm and steerable-nafx demos
are live examples of this pattern that you can run in the browser.
Note
This page builds on Custom Audio Worklets. A custom pre/post-processor must be instantiated on the audio worklet thread, so you always need a small custom worklet file.
Two-Step Setup¶
The processor lives in two places. On the main thread, you create
a JSPrePostProcessor from the factory — this tells anira
that pre/post-processing will be handled in JavaScript:
const ppProcessor = aniraWeb.JSPrePostProcessor(inferenceConfig)
const inferenceHandler = aniraWeb.InferenceHandler(ppProcessor, inferenceConfig)
On the audio worklet thread, you reconstruct the C++ processor as
your subclass and register it. createFromPointer wraps the existing
C++ instance (state.prePostProcessorPtr) so JS overrides hook into
the same object:
// audio-worklet.ts
import {
AniraAudioWorkletBase,
type AniraWorkletState,
} from '@anira-project/anira/workers/worklet-base'
import { JSPrePostProcessor } from '@anira-project/anira'
class MyPrePostProcessor extends JSPrePostProcessor {
// overrides go here
}
class MyWorklet extends AniraAudioWorkletBase {
protected async onConfigured(state: AniraWorkletState) {
const { aniraWeb, prePostProcessorPtr } = state
const ppProcessor = MyPrePostProcessor.createFromPointer(
aniraWeb.getWasmInstance(),
prePostProcessorPtr
)
this.prePostRegistry.set(prePostProcessorPtr, ppProcessor)
}
}
registerProcessor('my-worklet', MyWorklet)
What You Can Override¶
JSPrePostProcessor exposes the same hooks as the C++ class:
Method |
When it runs |
|---|---|
|
Before each inference call. Use it to pull samples from the input ring buffers into the model’s input tensors. |
|
After each inference call. Use it to push the model’s output tensors into the output ring buffers. |
Inside an override you can read and write non-streamable tensor values
with getInput / setInput / getOutput / setOutput (same
semantics as the C++ class — see Usage Guide), and call into raw
WASM exports through this.wasmInstance for ring-buffer manipulation.
In Practice: Gain Clamp¶
This is the smallest possible custom pre-processor: it clamps the gain
parameter to [0, 1] before passing it to the C++ pre-processing.
// audio-worklet.ts
import {
AniraAudioWorkletBase,
type AniraWorkletState,
} from '@anira-project/anira/workers/worklet-base'
import {
JSPrePostProcessor,
type PossiblePointer,
type VectorBufferF,
type VectorRingBuffer,
} from '@anira-project/anira'
class GainClampPrePostProcessor extends JSPrePostProcessor {
override preProcess(
ringBuffers: PossiblePointer<VectorRingBuffer>,
buffers: PossiblePointer<VectorBufferF>,
backend: number
): void {
const gain = this.getInput(0, 1)
this.setInput(Math.min(1.0, gain), 0, 1)
super.preProcess(ringBuffers, buffers, backend)
}
}
class PrePostProcessorWorklet extends AniraAudioWorkletBase {
protected async onConfigured(state: AniraWorkletState) {
const { aniraWeb, prePostProcessorPtr } = state
const ppProcessor = GainClampPrePostProcessor.createFromPointer(
aniraWeb.getWasmInstance(),
prePostProcessorPtr
)
this.prePostRegistry.set(prePostProcessorPtr, ppProcessor)
}
}
registerProcessor('pre-post-processors', PrePostProcessorWorklet)
The setup is identical to the one on the Basic Usage page
except that PrePostProcessor becomes JSPrePostProcessor and
configureAudioWorklet is given the processor name:
const ppProcessor = aniraWeb.JSPrePostProcessor(inferenceConfig)
ppProcessor.setInput(1, 0, 1)
await aniraWeb.registerAudioWorkletForContext(
audioContext,
new URL('./audio-worklet.ts', import.meta.url)
)
const inferenceNode = await aniraWeb.configureAudioWorklet(
audioContext,
inferenceHandler,
ppProcessor,
'pre-post-processors'
)
// The slider sets the raw gain on the main thread; the worklet thread
// clamps it on every block via the override above.
gainSlider.oninput = () => {
ppProcessor.setInput(parseFloat(gainSlider.value), 0, 1)
}
Pointer Arguments¶
preProcess and postProcess receive PossiblePointer<...>
arguments — either wrapper instances or raw WASM heap addresses. The
Architecture page covers the helpers (resolvePtr,
getPointer, wrapPointer) in full; the short version is to call
resolvePtr to get a numeric pointer, then call the exported WASM
functions on this.wasmInstance (e.g. _vector_ring_buffer_get,
_vector_buffer_f_get,
_prepostprocessor_pop_samples_from_buffer_window) to manipulate
buffers in place. The guitar-lstm and steerable-nafx demos show this pattern applied to real windowing
logic.
Note
Calling the underscore-prefixed exports directly looks unusual at
first, but it’s a deliberate performance escape hatch. Wrapping a
pointer into a TypeScript class (wrapPointer(BufferF, ptr),
etc.) allocates a JS object — fine on the main thread, but
unwanted allocation pressure in real-time code that runs every
audio block. The raw exports skip the wrapper entirely, at the
cost of dealing in numeric pointers. Reach for them in real-time
paths; stick with the wrappers everywhere else.