Phase 14: define parameterized native processors and portable configuration state
This commit is contained in:
parent
01d9db90fd
commit
c3eadee891
8 changed files with 419 additions and 1 deletions
10
Makefile
10
Makefile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
59
examples/processors.c
Normal 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
6
examples/processors.h
Normal 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
|
||||
59
include/xtc/xtc_processor.h
Normal file
59
include/xtc/xtc_processor.h
Normal 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
166
src/native/processor.c
Normal 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);
|
||||
}
|
||||
61
tests/unit/test_processor.c
Normal file
61
tests/unit/test_processor.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue