Phase 14: define parameterized native processors and portable configuration state

This commit is contained in:
Johannes Findeisen 2026-10-27 03:48:46 +01:00
commit c3eadee891
8 changed files with 419 additions and 1 deletions

View file

@ -15,7 +15,7 @@ CXXFLAGS ?= -O2 -g
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
LDFLAGS ?=
CORE_SOURCES := src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
CORE_SOURCES := src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
ifeq ($(JACK_AVAILABLE),yes)
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)
@ -294,3 +294,11 @@ $(BUILD_DIR)/test_recording: $(BUILD_DIR)/tests/integration/test_recording.o $(B
test-recording: $(BUILD_DIR)/test_recording
./$(BUILD_DIR)/test_recording
-include $(BUILD_DIR)/tests/integration/test_recording.d
$(BUILD_DIR)/test_processor: $(BUILD_DIR)/tests/unit/test_processor.o $(BUILD_DIR)/examples/processors.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
.PHONY: test-processor
test-processor: $(BUILD_DIR)/test_processor
./$(BUILD_DIR)/test_processor
test: test-processor
-include $(BUILD_DIR)/tests/unit/test_processor.d $(BUILD_DIR)/examples/processors.d

View file

@ -81,3 +81,54 @@ Tests cover ABI/size/field rejection, descriptor copying, aliased buffers,
prepare/process failures, stereo compile cleanup, retirement/destruction outside
processing, event offsets, seek note reset, source ports, instrument-buffer graph
routing and unsupported-save preservation. Run `make test`.
## Phase 14: structured parameter processors
`xtc_processor.h` is the public C99 parameter-only extension interface (ABI 2).
It complements the ABI 1 graph adapter without changing its layout or behavior.
`examples/processors.c` implements a mono gain effect and a monophonic MIDI sine
instrument using only public headers; `make test-processor` builds and exercises
both. The instrument has immediate note-off and no voice stealing/release tail.
Descriptors have an exact ABI version and structure size, a persistent identifier,
and up to 16 parameters with unique unsigned IDs, names, units, finite ranges and
defaults. Unsupported ABI versions/sizes are rejected before initialization.
Changing identifier, parameter order, IDs or ranges requires an explicit migration
by the extension author. There is no promise of binary compatibility across
compiler architectures, and no C++/FLTK types in either API.
Create prepares a copied descriptor on the control thread. Failed preparation
calls destroy, including with NULL: callbacks must implement partial cleanup.
Reset/process/parameter have one audio owner and must never allocate, free, block,
perform I/O or call GUI functions. Destroy is control-thread only after callback
quiescence. Reconfiguration prepares a new independent instance; publish it with
an appropriate owner handoff and retire the old instance after quiescence. The
callback code must remain loaded throughout that lifetime.
Each instance has one mono output and either one optional input (effect) or no
input (source/instrument). NULL input means silence. Stereo effects use two
independent instances; multichannel interdependent effects have no adapter.
Instruments receive at most 256 sorted sample-offset MIDI events per block.
The API intentionally has no channel-count argument: unsupported layouts cannot
be represented. Graph ABI 1 rejects instrument descriptors; explicitly render
instrument output and feed the existing instrument source buffers.
Up to 256 sorted parameter events split the process block at exact sample
positions. Equal-offset parameters execute in array order, then MIDI at that
sample. Values outside the descriptor range, bad IDs and malformed events reject
the entire call before touching state/output. Values are stepped; extension
callbacks own any smoothing. Transport discontinuities reset transient state;
reset retains current parameters. Callback errors silence the whole output and
reset voices/history; already applied parameters persist. Nonfinite/subnormal
successful samples become zero. Input/output may alias exactly.
`xtc_processor_save`/`restore` run only while quiescent and serialize configuration,
not DSP history. Format: `XTCN`, little-endian uint32 version 2, zero-padded
64-byte identifier, uint32 parameter count, zero uint32 reserved, then ordered
pairs of uint32 ID and IEEE float32 bits. Header is 80 bytes, pairs 8 bytes.
Restore validates exact length, identity, IDs, ranges and defaults/descriptor
compatibility before preparing a new instance; failure leaves existing handles
intact. Opaque assets and hidden state are outside this ABI. ABI 1 graph nodes
remain session-only and fail project saves explicitly; ABI 2 configuration blobs
are a separate public facility, not automatically embedded in project files.
GUI graph access continues through public C APIs only.

View file

@ -264,6 +264,14 @@ and playback of assets above the existing preload limit remain unqualified.
- Node state can be saved and restored where required.
- No C++ or FLTK types leak into the public C API.
Implemented: ABI 2 parameter-only C processor lifecycle with copied descriptors,
metadata/default/range validation, sample-offset parameter/MIDI ordering,
transactional portable configuration restoration, and public gain/instrument
examples. Lifecycle, invalid input, event ordering, in-place output, reset,
callback failure and state compatibility tests pass. See NATIVE_API.md. ABI 1
graph compatibility is retained; native graph persistence and opaque plugin
state are explicitly outside this parameter-only interface.
## Phase 15 — External Plugin Hosting
**Goal:** Add external plugin support only after the internal node lifecycle is reliable.

59
examples/processors.c Normal file
View file

@ -0,0 +1,59 @@
#include "processors.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
typedef struct { double rate, phase, frequency; float gain, velocity; unsigned pitch; } voice;
static xtc_result prepare(double rate, size_t frames, const float *values, size_t count, void **out)
{
voice *v = calloc(1, sizeof(*v));
(void)frames; (void)count;
*out = v;
if (!v) return XTC_ERR_OUT_OF_MEMORY;
v->rate = rate; v->gain = values[0];
return XTC_OK;
}
static void reset(void *state)
{ voice *v = state; v->phase = 0; v->velocity = 0; }
static void parameter(void *state, unsigned id, float value)
{ (void)id; ((voice *)state)->gain = value; }
static void destroy(void *state) { free(state); }
static xtc_result effect(void *state, const xtc_native_block *b)
{
voice *v = state; size_t i;
for (i = 0; i < b->frames; ++i) b->output[i] = b->input ? b->input[i] * v->gain : 0;
return XTC_OK;
}
static xtc_result instrument(void *state, const xtc_native_block *b)
{
voice *v = state; size_t i, e = 0;
for (i = 0; i < b->frames; ++i) {
while (e < b->event_count && b->events[e].offset == i) {
const xtc_midi_event *m = b->events + e++;
if (m->type == XTC_MIDI_NOTE_ON) {
v->pitch = m->pitch; v->velocity = (float)m->velocity / 127.0f;
v->frequency = 440.0 * pow(2.0, ((double)m->pitch - 69.0) / 12.0);
} else if (m->type == XTC_MIDI_ALL_OFF || m->pitch == v->pitch) v->velocity = 0;
}
b->output[i] = (float)sin(v->phase) * v->velocity * v->gain;
v->phase = fmod(v->phase + 6.283185307179586 * v->frequency / v->rate, 6.283185307179586);
}
return XTC_OK;
}
xtc_processor_descriptor example_effect(void)
{
xtc_processor_descriptor d;
memset(&d, 0, sizeof(d));
d.abi_version = XTC_PROCESSOR_ABI; d.struct_size = sizeof(d);
strcpy(d.identifier, "xtc.example.gain.v1"); d.kind = XTC_NATIVE_EFFECT;
d.parameter_count = 1; d.parameters[0].id = 1;
strcpy(d.parameters[0].name, "Gain"); strcpy(d.parameters[0].unit, "linear");
d.parameters[0].minimum = 0; d.parameters[0].maximum = 2; d.parameters[0].default_value = 1;
d.prepare = prepare; d.reset = reset; d.process = effect; d.parameter = parameter; d.destroy = destroy;
return d;
}
xtc_processor_descriptor example_instrument(void)
{
xtc_processor_descriptor d = example_effect();
strcpy(d.identifier, "xtc.example.sine.v1"); d.kind = XTC_NATIVE_INSTRUMENT; d.process = instrument;
return d;
}

6
examples/processors.h Normal file
View file

@ -0,0 +1,6 @@
#ifndef XTC_EXAMPLE_PROCESSORS_H
#define XTC_EXAMPLE_PROCESSORS_H
#include "xtc/xtc_processor.h"
xtc_processor_descriptor example_effect(void);
xtc_processor_descriptor example_instrument(void);
#endif

View file

@ -0,0 +1,59 @@
#ifndef XTC_PROCESSOR_H
#define XTC_PROCESSOR_H
#include "xtc_native.h"
#ifdef __cplusplus
extern "C" {
#endif
#define XTC_PROCESSOR_ABI 2U
#define XTC_PROCESSOR_PARAMETERS 16U
#define XTC_PROCESSOR_STATE_MAX (80U + 8U * XTC_PROCESSOR_PARAMETERS)
typedef struct {
unsigned int id;
char name[64], unit[16];
float minimum, maximum, default_value;
} xtc_parameter_info;
typedef struct {
unsigned int offset, id;
float value;
} xtc_parameter_event;
typedef struct {
unsigned int abi_version;
size_t struct_size;
char identifier[64];
xtc_native_kind kind;
size_t parameter_count;
xtc_parameter_info parameters[XTC_PROCESSOR_PARAMETERS];
xtc_result (*prepare)(double rate, size_t max_frames, const float *values, size_t count, void **state);
void (*reset)(void *state);
xtc_result (*process)(void *state, const xtc_native_block *block);
void (*parameter)(void *state, unsigned int id, float value);
void (*destroy)(void *state);
} xtc_processor_descriptor;
typedef struct xtc_processor xtc_processor;
/* Descriptors are copied. Trusted C callbacks, exact version/size matching,
* mono ports (stereo = two independent instances). Control: prepare/destroy;
* single audio owner: reset/process/parameter. No callback allocation/I/O/locks.
* Reset clears transient voices/history, retaining parameter values. */
xtc_result xtc_processor_validate(const xtc_processor_descriptor *descriptor);
xtc_result xtc_processor_create(const xtc_processor_descriptor *descriptor, double rate, size_t frames,
xtc_processor **out);
void xtc_processor_destroy(xtc_processor *processor);
void xtc_processor_reset(xtc_processor *processor);
/* Validate all events before touching output/state. At equal offsets parameter
* events run in array order before MIDI events. Up to 256 events of each type.
* A callback failure zeros the entire block and resets transient state.
* Parameter values already applied remain applied. No rollback of plugin DSP. */
xtc_result xtc_processor_process(xtc_processor *processor, const xtc_native_block *block,
const xtc_parameter_event *events, size_t count);
/* Quiescent control thread only. Portable little-endian config: identifier and
* stable parameter IDs/float32 values; no pointers, voices, delays or hidden DSP
* state. Restore prepares a new instance transactionally; old handles are intact.
* Descriptors requiring opaque assets/state are outside this parameter-only ABI.
* Format/ID/range mismatches are errors, never silent clamping. */
xtc_result xtc_processor_save(const xtc_processor *processor, void *data, size_t capacity, size_t *written);
xtc_result xtc_processor_restore(const xtc_processor_descriptor *descriptor, double rate, size_t frames,
const void *data, size_t size, xtc_processor **out);
#ifdef __cplusplus
}
#endif
#endif

166
src/native/processor.c Normal file
View file

@ -0,0 +1,166 @@
#include "xtc/xtc_processor.h"
#include "xtc/xtc_dsp.h"
#include <float.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
struct xtc_processor {
xtc_processor_descriptor descriptor;
void *state;
float values[XTC_PROCESSOR_PARAMETERS];
size_t frames;
uint64_t next;
};
static int text_valid(const char *s, size_t size, int empty)
{
size_t i;
if (!empty && !s[0]) return 0;
for (i = 0; i < size; ++i) {
if (!s[i]) return 1;
if ((unsigned char)s[i] < 32 || (unsigned char)s[i] > 126) return 0;
}
return 0;
}
xtc_result xtc_processor_validate(const xtc_processor_descriptor *d)
{
size_t i, j;
if (!d) return XTC_ERR_INVALID_ARGUMENT;
if (d->abi_version != XTC_PROCESSOR_ABI || d->struct_size != sizeof(*d)) return XTC_ERR_UNSUPPORTED;
if (!text_valid(d->identifier, 64, 0) || d->kind < XTC_NATIVE_EFFECT || d->kind > XTC_NATIVE_INSTRUMENT ||
d->parameter_count > XTC_PROCESSOR_PARAMETERS || !d->prepare || !d->reset || !d->process || !d->destroy ||
(d->parameter_count && !d->parameter)) return XTC_ERR_INVALID_ARGUMENT;
for (i = 0; i < d->parameter_count; ++i) {
const xtc_parameter_info *p = d->parameters + i;
if (!text_valid(p->name, sizeof(p->name), 0) || !text_valid(p->unit, sizeof(p->unit), 1) ||
!isfinite(p->minimum) || !isfinite(p->maximum) || !isfinite(p->default_value) ||
p->minimum > p->maximum || p->default_value < p->minimum || p->default_value > p->maximum)
return XTC_ERR_INVALID_ARGUMENT;
for (j = 0; j < i; ++j) if (d->parameters[j].id == p->id) return XTC_ERR_INVALID_ARGUMENT;
}
return XTC_OK;
}
static xtc_result create(const xtc_processor_descriptor *d, double rate, size_t frames,
const float *values, xtc_processor **out)
{
xtc_processor *p;
xtc_result result;
if (!out) return XTC_ERR_INVALID_ARGUMENT;
*out = NULL;
result = xtc_processor_validate(d);
if (result != XTC_OK) return result;
if (!isfinite(rate) || rate <= 0 || rate > 768000 || !frames || frames > 65536) return XTC_ERR_INVALID_ARGUMENT;
p = calloc(1, sizeof(*p));
if (!p) return XTC_ERR_OUT_OF_MEMORY;
p->descriptor = *d; p->frames = frames; p->next = UINT64_MAX;
if (values) memcpy(p->values, values, d->parameter_count * sizeof(float));
else { size_t i; for (i = 0; i < d->parameter_count; ++i) p->values[i] = d->parameters[i].default_value; }
result = d->prepare(rate, frames, p->values, d->parameter_count, &p->state);
if (result != XTC_OK) { xtc_processor_destroy(p); return result; }
d->reset(p->state);
*out = p;
return XTC_OK;
}
xtc_result xtc_processor_create(const xtc_processor_descriptor *d, double rate, size_t frames, xtc_processor **out)
{ return create(d, rate, frames, NULL, out); }
void xtc_processor_destroy(xtc_processor *p)
{ if (p) { p->descriptor.destroy(p->state); free(p); } }
void xtc_processor_reset(xtc_processor *p)
{ if (p) { p->descriptor.reset(p->state); p->next = UINT64_MAX; } }
static size_t parameter_index(const xtc_processor_descriptor *d, unsigned int id)
{
size_t i;
for (i = 0; i < d->parameter_count; ++i) if (d->parameters[i].id == id) return i;
return SIZE_MAX;
}
xtc_result xtc_processor_process(xtc_processor *p, const xtc_native_block *b, const xtc_parameter_event *events, size_t count)
{
size_t i, cursor = 0, param = 0, midi = 0;
xtc_result result = XTC_OK;
if (!p || !b || !b->output || b->position > UINT64_MAX - b->frames || b->frames > p->frames ||
count > 256 || (count && !events) || b->event_count > 256 || (b->event_count && !b->events) ||
(b->event_count && p->descriptor.kind != XTC_NATIVE_INSTRUMENT)) return XTC_ERR_INVALID_ARGUMENT;
for (i = 0; i < count; ++i) {
size_t index = parameter_index(&p->descriptor, events[i].id);
if (index == SIZE_MAX || events[i].offset >= b->frames || (i && events[i].offset < events[i-1].offset) ||
!isfinite(events[i].value) || events[i].value < p->descriptor.parameters[index].minimum ||
events[i].value > p->descriptor.parameters[index].maximum) return XTC_ERR_INVALID_ARGUMENT;
}
for (i = 0; i < b->event_count; ++i) {
const xtc_midi_event *e = b->events + i;
if (e->offset >= b->frames || (i && e->offset < b->events[i-1].offset) || e->type < XTC_MIDI_NOTE_ON ||
e->type > XTC_MIDI_ALL_OFF || e->pitch > 127 || e->velocity > 127 || e->channel > 15 ||
(e->type == XTC_MIDI_NOTE_ON && !e->velocity)) return XTC_ERR_INVALID_ARGUMENT;
}
if (!b->frames) return XTC_OK;
if (p->next != b->position) p->descriptor.reset(p->state);
while (cursor < b->frames) {
xtc_midi_event local[256];
size_t end, n = 0;
xtc_native_block segment;
while (param < count && events[param].offset == cursor) {
size_t index = parameter_index(&p->descriptor, events[param].id);
p->values[index] = events[param].value;
p->descriptor.parameter(p->state, events[param].id, events[param].value);
++param;
}
end = param < count ? events[param].offset : b->frames;
while (midi < b->event_count && b->events[midi].offset < end) {
local[n] = b->events[midi++]; local[n++].offset -= (unsigned int)cursor;
}
segment = (xtc_native_block){b->input ? b->input + cursor : NULL, b->output + cursor,
end - cursor, b->position + cursor, local, n};
result = p->descriptor.process(p->state, &segment);
if (result != XTC_OK) { memset(b->output, 0, b->frames * sizeof(float)); xtc_processor_reset(p); return result; }
cursor = end;
}
for (i = 0; i < b->frames; ++i) b->output[i] = xtc_dsp_finite(b->output[i]);
p->next = b->position + b->frames;
return result;
}
static void put(unsigned char *p, uint32_t value)
{ size_t i; for (i = 0; i < 4; ++i) p[i] = (unsigned char)(value >> (i * 8)); }
static uint32_t get(const unsigned char *p)
{ return (uint32_t)p[0] | (uint32_t)p[1]<<8 | (uint32_t)p[2]<<16 | (uint32_t)p[3]<<24; }
xtc_result xtc_processor_save(const xtc_processor *p, void *data, size_t capacity, size_t *written)
{
unsigned char *bytes = data;
size_t i, size;
if (!p || !written) return XTC_ERR_INVALID_ARGUMENT;
size = 80 + p->descriptor.parameter_count * 8;
*written = size;
if (!data || capacity < size) return XTC_ERR_CAPACITY;
if (sizeof(float) != 4 || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128) return XTC_ERR_UNSUPPORTED;
memset(bytes, 0, size); memcpy(bytes, "XTCN", 4); put(bytes+4, 2);
memcpy(bytes+8, p->descriptor.identifier, strlen(p->descriptor.identifier));
put(bytes+72, (uint32_t)p->descriptor.parameter_count);
for (i = 0; i < p->descriptor.parameter_count; ++i) {
uint32_t bits; memcpy(&bits, p->values+i, 4);
put(bytes+80+i*8, p->descriptor.parameters[i].id); put(bytes+84+i*8, bits);
}
return XTC_OK;
}
xtc_result xtc_processor_restore(const xtc_processor_descriptor *d, double rate, size_t frames,
const void *data, size_t size, xtc_processor **out)
{
const unsigned char *bytes = data;
float values[XTC_PROCESSOR_PARAMETERS] = {0};
size_t i, j;
xtc_result result;
if (!out) return XTC_ERR_INVALID_ARGUMENT;
*out = NULL;
result = xtc_processor_validate(d);
if (result != XTC_OK) return result;
if (sizeof(float) != 4 || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128) return XTC_ERR_UNSUPPORTED;
if (!bytes || size != 80 + 8*d->parameter_count || memcmp(bytes, "XTCN", 4) || get(bytes+4) != 2 ||
!memchr(bytes+8, 0, 64) || strcmp((const char *)bytes+8, d->identifier) ||
get(bytes+72) != d->parameter_count || get(bytes+76)) return XTC_ERR_FORMAT;
for (i = 0; i < d->parameter_count; ++i) {
uint32_t bits = get(bytes+84+i*8);
if (get(bytes+80+i*8) != d->parameters[i].id) return XTC_ERR_UNSUPPORTED;
memcpy(values+i, &bits, 4);
if (!isfinite(values[i]) || values[i] < d->parameters[i].minimum || values[i] > d->parameters[i].maximum)
return XTC_ERR_FORMAT;
}
for (j = 8 + strlen((const char *)bytes+8); j < 72; ++j) if (bytes[j]) return XTC_ERR_FORMAT;
return create(d, rate, frames, values, out);
}

View file

@ -0,0 +1,61 @@
#include "../../examples/processors.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
static int destroyed;
static xtc_result fail(double r, size_t n, const float *v, size_t c, void **s)
{ (void)r; (void)n; (void)v; (void)c; *s = NULL; return XTC_ERR_OUT_OF_MEMORY; }
static void cleanup(void *s) { assert(!s); ++destroyed; }
static xtc_result process_fail(void *s, const xtc_native_block *b)
{ (void)s; b->output[0] = 99; return XTC_ERR_UNSUPPORTED; }
int main(void)
{
xtc_processor_descriptor d = example_effect(), bad;
xtc_processor *p = NULL, *restored = NULL;
float out[16], input[16]; size_t i, size;
unsigned char state[XTC_PROCESSOR_STATE_MAX], broken[XTC_PROCESSOR_STATE_MAX];
xtc_native_block b = {input, out, 16, 0, NULL, 0};
xtc_parameter_event e[] = {{0,1,.5f},{7,1,1},{7,1,2}};
for (i = 0; i < 16; ++i) input[i] = 1;
bad = d; bad.abi_version = 99; assert(xtc_processor_validate(&bad) == XTC_ERR_UNSUPPORTED);
bad = d; bad.parameters[0].default_value = NAN; assert(xtc_processor_validate(&bad) != XTC_OK);
bad = d; bad.prepare = fail; bad.destroy = cleanup;
assert(xtc_processor_create(&bad,48000,16,&p) == XTC_ERR_OUT_OF_MEMORY && !p && destroyed == 1);
assert(xtc_processor_create(&d,48000,16,&p) == XTC_OK);
assert(xtc_processor_process(p,&b,e,3) == XTC_OK);
for (i = 0; i < 16; ++i) assert(out[i] == (i < 7 ? .5f : 2));
e[1].value = 3; out[0] = 123;
assert(xtc_processor_process(p,&b,e,3) == XTC_ERR_INVALID_ARGUMENT && out[0] == 123);
assert(xtc_processor_save(p,state,1,&size) == XTC_ERR_CAPACITY && size == 88);
assert(xtc_processor_save(p,state,sizeof(state),&size) == XTC_OK);
assert(xtc_processor_restore(&d,48000,16,state,size,&restored) == XTC_OK);
b.input = out; for (i = 0; i < 16; ++i) out[i] = 1;
assert(xtc_processor_process(restored,&b,NULL,0) == XTC_OK && out[15] == 2);
xtc_processor_destroy(restored); restored = NULL;
memcpy(broken,state,size); broken[8] = 'z';
assert(xtc_processor_restore(&d,48000,16,broken,size,&restored) != XTC_OK && !restored);
memcpy(broken,state,size); broken[80] = 42;
assert(xtc_processor_restore(&d,48000,16,broken,size,&restored) != XTC_OK);
assert(xtc_processor_restore(&d,48000,16,state,size-1,&restored) != XTC_OK);
xtc_processor_destroy(p);
d.process = process_fail; assert(xtc_processor_create(&d,48000,16,&p) == XTC_OK);
assert(xtc_processor_process(p,&b,NULL,0) == XTC_ERR_UNSUPPORTED);
for (i = 0; i < 16; ++i) assert(out[i] == 0);
xtc_processor_destroy(p);
d = example_instrument(); assert(xtc_processor_create(&d,48000,16,&p) == XTC_OK);
{
xtc_midi_event midi[] = {{0,XTC_MIDI_NOTE_ON,0,69,127},{8,XTC_MIDI_NOTE_OFF,0,69,0}};
b.input = NULL; b.events = midi; b.event_count = 2;
e[0].value = .25f;
assert(xtc_processor_process(p,&b,e,1) == XTC_OK);
assert(out[1] > 0 && out[1] < .25f);
for (i = 8; i < 16; ++i) assert(out[i] == 0);
xtc_processor_reset(p); b.events = NULL; b.event_count = 0;
assert(xtc_processor_process(p,&b,NULL,0) == XTC_OK);
for (i = 0; i < 16; ++i) assert(out[i] == 0);
}
xtc_processor_destroy(p);
puts("Native parameter events, state, effect/instrument and lifecycle errors passed.");
return 0;
}