Usage Guide#
Overview#
Anira provides the following structures and classes to help you integrate real-time audio processing with your machine learning models:
Class |
Description |
|---|---|
Manages audio processing/inference for the real-time thread, offloading inference to the thread pool and updating the real-time thread buffers with processed audio. This class provides the main interface for interacting with the library. |
|
A configuration structure for defining model specifics such as input/output shape, model details such as maximum inference time, and more. Each InferenceHandler instance must be constructed with this configuration. |
|
Enables pre- and post-processing steps before and after inference. Either use the default PrePostProcessor or inherit from this class for custom processing. |
|
A structure for defining the host configuration: buffer size and sample rate. |
|
Optional: The configuration structure that defines the context
across all anira instances. Here you can define the behaviour of the
thread pool, such as the number of threads and how idle threads wait
for new work (see |
1. Inference Configuration#
Start by specifying your model configuration using anira::InferenceConfig. This includes the model path, input/output shapes, and other critical settings that match the requirements of your model.
1.1. ModelData#
First define the model information and the corresponding inference backend in a anira::ModelData. There are two ways to define the model information:
Pass the model path as a string:
{std::string model_path, anira::InferenceBackend backend}
Pass the model data as binary information:
{void* model_data, size_t model_size, anira::InferenceBackend backend}
Note
Defining the model data as binary information is only possible for the anira::InferenceBackend::ONNX and anira::InferenceBackend::TFLITE until now.
The anira::InferenceConfig requires a vector of anira::ModelData.
std::vector<anira::ModelData> model_data = {
{"path/to/your/model.pt", anira::InferenceBackend::LIBTORCH},
{"path/to/your/model.onnx", anira::InferenceBackend::ONNX},
{"path/to/your/model.tflite", anira::InferenceBackend::TFLITE}
};
Note
It is not necessary to submit a model for each backend anira was built with, only the one you want to use.
1.2. TensorShape#
In the next step, define the input and output shapes of the model in an anira::TensorShape. The input and output_shapes are defined as anira::TensorShapeList, where each inner vector represents the shape of a tensor.
{anira::TensorShapeList input_shape, anira::TensorShapeList output_shape, (optional) anira::InferenceBackend}
The input and output shapes are defined as a vector of integers, where each integer represents the size of a dimension in the tensor. The optional anira::InferenceBackend parameter allows you to specify which backend this shape corresponds to. If you do not specify the backend, the shape is used for all backends that do not have a specific shape defined.
The anira::InferenceConfig requires a vector of anira::TensorShape.
std::vector<anira::TensorShape> tensor_shape = {
{{{1, 4, 15380}, {1, 1}}, {{1, 1, 2048}, {1, 1}}, anira::InferenceBackend::LIBTORCH},
{{{1, 4, 15380}, {1, 1}}, {{1, 1, 2048}, {1, 1}}, anira::InferenceBackend::ONNX},
{{{1, 15380, 4}, {1, 1}}, {{1, 2048, 1}, {1, 1}}, anira::InferenceBackend::TFLITE}
};
Note
If the input and output shapes of the model are the same for all backends, you can also define only one anira::TensorShape without a specific anira::InferenceBackend:
1.3. (Optional) ProcessingSpec#
In some cases, you may want to define a processing specification that describes how the model should be processed. This is optional and can be used to specify additional parameters for the inference process. Here you can define the number of input and output channels and also whether tensors shall be streamable or non-streamable.
The following parameters can be defined in the anira::ProcessingSpec:
Parameter |
Description |
|---|---|
preprocess_input_channels |
Type: |
postprocess_output_channels |
Type: |
preprocess_input_size |
Type: |
postprocess_output_size |
Type: |
internal_model_latency |
Type: |
You only need to define the parameters that are relevant for your model. If you do not define an anira::ProcessingSpec, the default values will be used. Here is an example of how to define the anira::ProcessingSpec with all parameters:
std::vector<anira::ProcessingSpec> processing_spec = {
{4, 1}, // Input tensor 0 has 4 input channels, and tensor 1 has 1 input channel
{1, 1}, // Output tensor 0 has 1 output channel, and tensor 1 has 1 output channel
{2048, 0}, // Preprocess input size is 2048 for tensor 0 and 0 for tensor 1
{2048, 0}, // Postprocess output size is 2048 for tensor 0 and 0 for tensor 1
{0, 0} // Internal model latency is 0 for both tensors, meaning no internal latency
};
1.4. InferenceConfig#
Finally, define the necessary anira::InferenceConfig with the vector of anira::ModelData, vector of anira::TensorShape, the optional anira::ProcessingSpec, and the maximum inference time. The maximum inference time is the measured worst case inference time. If the inference time during execution exceeds this value, it is likely that the audio signal will contain dropouts. There are also some other optional parameters that can be set in the anira::InferenceConfig to further customize the inference process.
anira::InferenceConfig inference_config (
model_data, // std::vector<anira::ModelData>
tensor_shape, // std::vector<anira::TensorShape>
processing_spec, // anira::ProcessingSpec (optional)
42.66f // Maximum inference time in ms
);
These are the other optional parameters that can be set in the anira::InferenceConfig:
Parameter |
Description |
|---|---|
warm_up |
Type: |
session_exclusive_processor |
Type: |
blocking_ratio |
Type: |
num_parallel_processors |
Type: |
1.5. (Optional) Loading the Configuration from JSON#
Instead of constructing the anira::InferenceConfig (and the optional anira::ContextConfig) in C++, you can describe them in a JSON file and load them at runtime with anira::JsonConfigLoader. This keeps model paths, tensor shapes and processing parameters out of the compiled binary, so you can swap models without recompiling.
The JSON mirrors the two configuration structs — a context_config object and an inference_config object:
{
"context_config": {
"num_threads": 1,
"wait_strategy": "spin_backoff",
"log_level": "warning"
},
"inference_config": {
"model_data": [
{ "model_path": ".../simple_gain_network_mono.pt", "inference_backend": "LIBTORCH" },
{ "model_path": ".../simple_gain_network_mono.onnx", "inference_backend": "ONNX" },
{ "model_path": ".../simple_gain_network_mono.tflite", "inference_backend": "TFLITE" }
],
"tensor_shape": [
{
"input_shape": [[1, 1, 512], [1]],
"output_shape": [[1, 1, 512], [1]]
}
],
"processing_spec": {
"preprocess_input_channels": [1, 1],
"postprocess_output_channels": [1, 1],
"preprocess_input_size": [512, 0],
"postprocess_output_size": [512, 0]
},
"max_inference_time": 5.0,
"warm_up": 1
}
}
The keys map directly onto the fields described above:
context_config→anira::ContextConfig:num_threads(unsigned integer),wait_strategy("spin_backoff"or"blocking", seeanira::WaitStrategy) andlog_level("debug","info","warning"or"error", seeanira::LogLevel). The whole block is optional. On WebAssembly builds"blocking"is not supported and is coerced to"spin_backoff"with a warning; likewise anum_threadsother than0is coerced to0with a warning — the context cannot run inference threads on the web, they are always created from JavaScript viaAniraWeb.spinUpInferenceWorker().inference_config.model_data→ the vector ofanira::ModelData; each entry needs amodel_pathand aninference_backend(one ofLIBTORCH,ONNX,TFLITE,LITERT,EXECUTORCH), and optionally amodel_functionnaming the entry point to run for LibTorch and ExecuTorch (both formats can carry several named methods in one file, e.g. RAVE’sencode/decode; ExecuTorch defaults toforward).inference_config.tensor_shape→ the vector ofanira::TensorShape. For a single-tensor model the shapes may be given as a flat array ([1, 1, 2048]); for multi-tensor models use a list of per-tensor shapes ([[1, 1, 512], [1]]).inference_config.processing_spec→ the optionalanira::ProcessingSpec(preprocess_input_channels,postprocess_output_channels,preprocess_input_size,postprocess_output_sizeand the optionalinternal_model_latency).max_inference_time,warm_up,session_exclusive_processor,blocking_ratioandnum_parallel_processors→ the InferenceConfig parameters from the table above.
Then load the file and move the configurations out of the loader:
#include <anira/anira.h>
anira::JsonConfigLoader json_config_loader("path/to/Config.json");
anira::ContextConfig context_config = std::move(*json_config_loader.get_context_config());
anira::InferenceConfig inference_config = std::move(*json_config_loader.get_inference_config());
anira::PrePostProcessor pp_processor(inference_config);
anira::InferenceHandler inference_handler(pp_processor, inference_config, context_config);
Note
anira::JsonConfigLoader::get_context_config() and anira::JsonConfigLoader::get_inference_config() each return a std::unique_ptr; move the value out (as above) before using it. The loader also accepts a std::istream, so the configuration can be loaded from an embedded resource instead of a file on disk.
Tip
See the JUCE plugin example (MODEL_TO_USE == 8), which loads the RAVE model entirely from RaveFunkDrumConfig.json via anira::JsonConfigLoader.
2. Pre and Post Processing#
For most use cases, you can use the default anira::PrePostProcessor without modification. This is suitable when your model operates in the time domain with straightforward input/output tensor shapes.
// Create an instance of anira::PrePostProcessor
anira::PrePostProcessor pp_processor(inference_config);
If your model requires custom pre- or post-processing (such as frequency domain transforms, custom windowing, or multi-tensor operations), you can create a custom preprocessor by inheriting from the anira::PrePostProcessor class. For detailed information on implementing custom preprocessing and postprocessing, see the Custom Pre/Post Processing chapter.
3. Inference Handler#
In your application, you will need to create an instance of the anira::InferenceHandler class. This class is responsible for managing the inference process, including threading and real-time constraints. The constructor takes as arguments an instance of the default or custom anira::PrePostProcessor and an instance of the anira::InferenceConfig structure.
// Sample initialization in your application's initialization function
// Default PrePostProcessor
anira::PrePostProcessor pp_processor(inference_config);
// or custom PrePostProcessor
CustomPrePostProcessor pp_processor(inference_config);
// Create an InferenceHandler instance
anira::InferenceHandler inference_handler(pp_processor, inference_config);
3.1. (Optional) ContextConfig#
If you want to define a custom context configuration, you can do so by creating an instance of the anira::ContextConfig structure. This structure allows you to define the behaviour of the thread pool — the number of threads and how idle threads wait for new work — as well as the log level of anira and its inference backends.
// Use the existing anira::InferenceConfig and anira::PrePostProcessor instances
// Create an instance of anira::ContextConfig
anira::ContextConfig context_config {
4, // Number of threads
anira::WaitStrategy::Blocking, // Idle threads block instead of polling
anira::LogLevel::Warning // Only report warnings and errors
};
// Create an InferenceHandler instance
anira::InferenceHandler inference_handler(pp_processor, inference_config, context_config);
The wait strategy (anira::WaitStrategy) controls what an inference thread does while the shared inference queue is empty:
anira::WaitStrategy::SpinBackoff(default): the thread polls the queue with an exponential backoff — a short hot-spin phase, then a yield/sleep loop with a period of roughly 100 µs. This gives the lowest possible pickup latency when new work arrives within microseconds of the thread going idle, at the cost of continuous polling syscalls and CPU wakeups for as long as the thread is idle.anira::WaitStrategy::Blocking: the thread blocks on the queue’s semaphore and is woken directly by the enqueue. Idle threads consume no CPU, and the wakeup arrives immediately (typically within a few microseconds via a futex/semaphore signal). In exchange, the submitting thread pays one bounded, non-blocking semaphore signal per submission when a consumer is asleep — the same class of wakeup that audio servers like JACK and PipeWire issue from their real-time threads on every cycle.
For models whose inference time dominates the round trip, the throughput of both strategies is identical within measurement noise — choose Blocking to eliminate idle CPU/power usage, and SpinBackoff only when sub-microsecond work-pickup latency matters.
Note
All anira instances in a process share one inference thread pool, so only one wait strategy can be in effect per process — the one of the first-created context. If a later instance requests a different strategy, the request is ignored and anira logs a warning. Since both strategies produce identical results, a mismatch is harmless; the warning only tells you that the requested performance characteristic is not the one in effect.
Note
On WebAssembly builds blocking waits are impossible — inference loops are driven cooperatively by JS Workers — so anira::WaitStrategy::Blocking is coerced to SpinBackoff with a warning, both by anira::JsonConfigLoader and by the context itself.
The log level (anira::LogLevel) is one setting for the whole inference stack: it gates anira’s own log output and is forwarded to the logging facilities of the enabled backends — the ONNX Runtime environment severity, the LiteRT environment min-logger severity and the LibTorch/c10 log level (TFLite and ExecuTorch excepted — their prebuilt runtimes offer no runtime logging control). A message is emitted when its severity is at or above the configured level; the available levels are Debug, Info, Warning and Error, where Debug additionally enables the backends’ verbose output. The default is LogLevel::Info in debug builds and LogLevel::Error in release builds.
Note
Like the thread pool, the log level is process-global. If the ContextConfigs in a process disagree, the lowest (most verbose) requested level wins — no session can silence the diagnostics another session asked for — and the mismatch is reported with a warning. The TFLite backend is exempt from the log level — the prebuilt TFLite C library does not export any runtime logging control, so its (rare) log lines are unaffected.
You can also opt out of the auto-managed thread pool entirely and supply your own threads. Pass 0 to anira::ContextConfig so the auto-pool stays empty, then create as many threads as you want via anira::Context::make_inference_thread(), call start() on each, and either call stop() or simply destroy the returned unique_ptr to tear them down.
anira::ContextConfig context_config { 0 }; // opt out of the auto-pool
anira::InferenceHandler inference_handler(pp_processor, inference_config, context_config);
auto thread = anira::Context::make_inference_thread();
thread->start();
// ... process audio ...
thread->stop(); // or just let `thread` go out of scope
4. Get ready for Processing#
Before processing audio data, the anira::InferenceHandler::prepare() method of the anira::InferenceHandler instance must be called. This allocates all necessary memory in advance. The anira::InferenceHandler::prepare() method needs an instance of anira::HostConfig which defines the buffer size and sample rate of the host application. The active inference backend defaults to the first model in your anira::InferenceConfig whose backend is available in the build (or to CUSTOM when a custom processor was passed to the constructor); to run a different backend, select it with the anira::InferenceHandler::set_inference_backend() method.
4.1. HostConfig#
The anira::HostConfig structure defines the host application’s configuration, including buffer size and sample rate. This configuration is essential for the anira::InferenceHandler to allocate appropriate memory and calculate processing latency.
To construct anira::HostConfig, provide the buffer size and sample rate for a specific streamable input tensor. By default, tensor index 0 is used. For models with multiple input tensors, specify the desired tensor index.
The structure also includes an optional parameter that controls whether the buffer size is seen as static or as the maximum buffer size. When this parameter is set to true, variable buffer sizes smaller than the specified maximum are allowed, which is useful for real-time applications with dynamic buffer sizes. However, this may increase the latency that anira calculates, since it needs to compensate for all possible size variations.
Create HostConfig with static buffer size for input tensor 0:
anira::HostConfig host_config {
2048.f, // Buffer size in samples
44100.f // Sample rate in Hz
};
Create HostConfig with maximum buffer size for input tensor 1:
anira::HostConfig host_config {
2048.f, // Buffer size in samples
44100.f, // Sample rate in Hz
true, // Allow smaller buffer sizes (optional, default is false)
1 // Tensor index (optional, default is 0)
};
Note
The buffer size parameter accepts floating-point values, allowing you to specify fractional relationships between the host buffer and the model processing buffer. For example, setting a buffer size of 0.5f means the anira::InferenceHandler will receive one sample for the specified input tensor every two host buffer cycles. The latency calculation in anira accounts for this, assuming the sample is provided during the second host buffer cycle (worst-case scenario). If your model produces output at twice the input rate, the anira::InferenceHandler can return one sample per host buffer cycle.
4.2. Prepare#
The anira::InferenceHandler::prepare() method is called with an instance of anira::HostConfig to allocate the necessary memory for the inference process. This method must be called before processing audio data. You can optionally specify the latency compensation for the inference process by passing a latency value in samples for a specific output tensor or a vector of latency values for all output tensors. If you do not specify a latency value, anira will calculate a minimal latency based on the information in the anira::HostConfig and the anira::InferenceConfig. This latency calculation is quite sophisticated and you can read more about it in the Latency section.
Preparing without custom latency (automatic latency calculation):
// Prepare the :cpp with automatic latency calculation
inference_handler.prepare(host_config);
Preparing with custom latency for a specific output tensor:
// Prepare with custom latency for the first output tensor (index 0)
size_t custom_latency_samples = 1024;
size_t output_tensor_index = 0;
inference_handler.prepare(host_config, custom_latency_samples, output_tensor_index);
Preparing with custom latency for all output tensors:
// Prepare with custom latency values for all output tensors
std::vector<size_t> custom_latency_values = {1024, 512}; // Different latency for each tensor
inference_handler.prepare(host_config, custom_latency_values);
Note
Only streamable tensors can have a latency != 0. Non-streamable tensors are available via the anira::PrePostProcessor::get_output() method and do not require a latency value.
4.3. Select Backend#
Before processing audio, you must select which inference backend to use. The available backends depend on which ones were enabled during the build process. You can choose from:
anira::InferenceBackend::LIBTORCH- PyTorch/LibTorch modelsanira::InferenceBackend::ONNX- ONNX Runtime modelsanira::InferenceBackend::TFLITE- TensorFlow Lite modelsanira::InferenceBackend::CUSTOM- Custom backend implementations
The first configured model’s backend is selected automatically; to run another one, select the backend that corresponds to your model format:
// Select the inference backend (optional — defaults to the first configured model)
inference_handler.set_inference_backend(anira::InferenceBackend::ONNX);
Note
Please refer to the Custom Backend Definition section for more information on how to implement your own custom backend.
5. Real-time Processing#
Now we are ready to process audio in the process callback of our real-time audio application. For streamable as well as non-streamable tensors, the anira::InferenceHandler::process() or the anira::InferenceHandler::push_data() and anira::InferenceHandler::pop_data() methods can be used to process audio data. All methods can be used in the real-time thread. Each function is overloaded so it can be used with a single tensor or with a vector of tensors.
5.1. Process Method#
The anira::InferenceHandler::process() method is the most straightforward approach for real-time audio processing when input and output happen simultaneously.
Simple In-Place Processing:
For models where input and output have the same shape and only one tensor is streamable:
// In your real-time audio callback
void processBlock(float** audio_data, int num_samples) {
// Process audio in-place - input is overwritten with output
size_t processed_samples = inference_handler.process(
audio_data,
num_samples
);
// audio_data now contains the processed audio samples
}
Separate Input/Output Buffers:
For models where the input and output shapes differ or when you want to keep input and output separate:
void processBlock(float** input_audio, float** output_audio, int num_samples) {
size_t output_samples = inference_handler.process(
input_audio, // const float* const* - input data
num_samples, // number of input samples
output_audio, // float* const* - output buffer
output_buffer_size // maximum output buffer size
);
// output_samples contains the actual number of samples written
}
Multi-Tensor Processing:
For models with multiple input and output tensors (e.g., audio + control parameters):
// Prepare input and output data for multiple tensors in initialization
const float* const* const* input_data = new const float* const*[2];
float* const* const* output_data = new float* const*[2];
void processBlock(float** audio_input, float* control_params,
float** audio_output, float* confidence_output,
int num_audio_samples) {
input_data[0] = audio_input; // Tensor 0: audio data
input_data[1] = (const float* const*) &control_params; // Tensor 1: control parameters
output_data[0] = audio_output; // Tensor 0: processed audio
output_data[1] = (float* const*) &confidence_output; // Tensor 1: confidence scores
// Specify number of samples for each tensor
size_t input_samples[] = {num_audio_samples, 4}; // Audio samples, 4 control values
size_t output_samples[] = {num_audio_samples, 1}; // Audio samples, 1 confidence value
// Process all tensors simultaneously
size_t* processed_samples = inference_handler.process(
input_data, input_samples,
output_data, output_samples
);
}
// Clean up
delete[] input_data;
delete[] output_data;
5.2. Push/Pop Data Method#
The anira::InferenceHandler::push_data() and anira::InferenceHandler::pop_data() methods enable decoupled processing where input and output operations are separated. This is particularly useful for:
Models with different input/output timing requirements
Buffered processing scenarios
Warning
The anira::InferenceHandler::push_data() and anira::InferenceHandler::pop_data() methods should only be called from the same thread. Otherwise you may run into race conditions or other threading issues.
Basic Decoupled Processing:
void processBlock(float** input_audio, float** output_audio, int num_samples) {
// Push input data to the inference pipeline
inference_handler.push_data(
input_audio, // const float* const* - input data
num_samples, // number of input samples
0 // tensor index (optional, defaults to 0)
);
// Pop processed output data from the pipeline
size_t received_samples = inference_handler.pop_data(
output_audio, // float* const* - output buffer
num_samples, // maximum number of output samples
0 // tensor index (optional, defaults to 0)
);
// received_samples contains the actual number of samples retrieved
}
Multi-Tensor Decoupled Processing:
// Prepare input and output data for multiple tensors in initialization
const float* const* const* input_data = new const float* const*[2];
float* const* const* output_data = new float* const*[2];
void processBlock(float** audio_input, float* control_params,
float** audio_output, float* confidence_output,
int num_audio_samples) {
// Push data for multiple tensors
input_data[0] = audio_input;
input_data[1] = (const float* const*) &control_params;
size_t input_samples[] = {num_audio_samples, 4};
inference_handler.push_data(input_data, input_samples);
// Pop data for multiple tensors
output_data[0] = audio_output;
output_data[1] = (float* const*) &confidence_output;
size_t output_samples[] = {num_audio_samples, 1};
size_t* received_samples = inference_handler.pop_data(output_data, output_samples);
}
// Clean up
delete[] input_data;
delete[] output_data;
Note
The anira::InferenceHandler::pop_data() method supports a wait_until parameter for blocking until data is available or timeout occurs. Use with the blocking_ratio in anira::InferenceConfig for proper latency compensation. Note that this blocks the real-time thread and is not fully lock-free, but this enables you to further reduce latency by waiting for the next available data.
5.3. Processing Non-Streamable Tensors#
Some neural networks require additional input parameters or output values that do not need to be time-aligned and can therefore be updated asynchronously with the host buffers. For non-streamable tensors (those with preprocess_input_size or postprocess_output_size set to 0), you can use the anira::PrePostProcessor methods to submit or retrieve additional values.
Setting and Getting Non-Streamable Values:
// In your custom PrePostProcessor or directly via the :cpp
// Set input values for non-streamable tensors
pp_processor.set_input(gain_value, tensor_index, sample_index);
pp_processor.set_input(threshold_value, tensor_index, sample_index + 1);
// Get output values from non-streamable tensors
float confidence_score = pp_processor.get_output(tensor_index, sample_index);
float peak_gain = pp_processor.get_output(tensor_index, sample_index + 1);
Example: Audio Effect with Control Parameters:
void processBlock(float** audio_data, int num_samples,
float gain_param, float threshold_param) {
// Set control parameters for non-streamable tensor (tensor index 1)
pp_processor.set_input(gain_param, 1, 0);
pp_processor.set_input(threshold_param, 1, 1);
// Process audio (tensor index 0 is streamable audio data)
inference_handler.process(audio_data, num_samples);
// Retrieve computed values from non-streamable output tensor (tensor index 1)
float computed_peak_gain = pp_processor.get_output(1, 0);
float signal_energy = pp_processor.get_output(1, 1);
}
Note
The functions anira::PrePostProcessor::set_input() and anira::PrePostProcessor::get_output() can be called from any thread, allowing you to update control parameters or retrieve additional values asynchronously without blocking the real-time audio processing thread.
5.4. Resetting the Stream#
anira::InferenceHandler::reset() re-anchors the inference pipeline to its initial state: it clears all internal buffers, re-seeds the latency zero-padding, and invalidates every inference dispatched so far — results still in flight are discarded and their internal structures reclaimed automatically. This is useful whenever the processed stream loses continuity, e.g. on transport jumps, playback restarts, or onset/transient re-synchronization.
// Safe on the audio thread, e.g. to realign the inference grid mid-stream
inference_handler.reset();
The call is wait-free and real-time safe for all session configurations, including stateful (session_exclusive_processor) ones — it never sleeps, locks, allocates, or performs a syscall, and is annotated [[clang::nonblocking]] in RealtimeSanitizer builds. Call it from the thread that drives anira::InferenceHandler::process() (or anira::InferenceHandler::push_data() / anira::InferenceHandler::pop_data()), or ensure no such call is concurrent — and never concurrently with anira::InferenceHandler::prepare() or destruction.
Note
anira::InferenceHandler::reset() does not wait for in-flight inferences to finish: an inference thread may still be executing a — discarded — inference after the call returns, including user code in a custom backend or the anira::PrePostProcessor::before_inference() / anira::PrePostProcessor::after_inference() hooks. If you need the guarantee that no inference thread touches shared state anymore (e.g. before mutating parameters such code reads), call anira::InferenceHandler::prepare() — which drains all in-flight work — or synchronize within your own backend.
Note
Until in-flight work finishes (bounded by one inference duration), its internal structures stay captive. If fresh data submitted in that window exhausts the remaining structure pool — likely on session-exclusive configurations, whose pools are small — the affected chunks complete as silence at their correct stream positions; the stream stays time-aligned and recovers by itself.
Note
Model-internal state (e.g. a recurrent hidden state inside the backend) is not reset — no anira reset has ever touched it. For stateful models, splice or clear such state via the anira::PrePostProcessor::before_inference() / anira::PrePostProcessor::after_inference() hooks.