delay_line example¶
A circular delay buffer with runtime-configurable length — the canonical pattern
for heap-allocated state with create_impl, reset_impl, and destroy_impl.
TL;DR — see it work first¶
Prerequisites¶
Or with pip if just-makeit is already installed:
What it demonstrates¶
- A heap-allocated circular buffer whose size is not a compile-time constant
- All three lifecycle hooks:
create_impl,reset_impl,destroy_impl - Mixing opaque (
float *) and scalar (uint32_t) state fields in one struct reset_implpreserving the buffer — reset zeros samples but keeps the allocation, solengthsurvives across calls toobj.reset()- The footgun to avoid: calling
set_length()after construction silently leaves the buffer at its original size (use a resize method instead)
1. Write the fragment¶
# delay_line.toml
[delay_line]
arg_type = "float"
return_type = "float"
mutable = "true"
create_impl = """
obj->taps = calloc(length, sizeof(float));
if (!obj->taps) { free(obj); return NULL; }
obj->length = length;
obj->idx = 0;
"""
reset_impl = """
memset(state->taps, 0, state->length * sizeof(float));
state->idx = 0;
"""
destroy_impl = "free(state->taps);"
[[delay_line.state]]
name = "taps"
type = "float *"
opaque = true
[[delay_line.state]]
name = "length"
type = "uint32_t"
default = "64"
[[delay_line.state]]
name = "idx"
type = "uint32_t"
default = "0"
Key points:
| Field | Kind | Scalar default? | Constructor param? | reset restores? |
|---|---|---|---|---|
taps |
opaque | — | No | Zeroed in place |
length |
scalar | 64 |
Yes | Preserved by reset_impl |
idx |
scalar | 0 |
No (generated ctor skips it) | Zeroed by reset_impl |
reset_impl runs instead of the auto-generated field-assignment body, so
it is responsible for everything — zeroing taps and resetting idx, but
explicitly not freeing or reallocating taps (the length survives).
2. Apply and build¶
just-makeit new delay_demo
cd delay_demo
just-makeit apply ../delay_line.toml
cmake -B build && cmake --build build
ctest --test-dir build
3. Implement step()¶
/* native/inc/delay_line/delay_line_core.h */
static inline float
delay_line_step(delay_line_state_t *state, float x)
{
/* Write new sample into ring buffer */
state->taps[state->idx] = x;
/* Tap at full delay distance */
uint32_t out_idx = (state->idx + 1) % state->length;
float y = state->taps[out_idx];
/* Advance write pointer */
state->idx = out_idx;
return y;
}
4. Use from Python¶
import numpy as np
from delay_demo import DelayLine
# 4-sample delay
dl = DelayLine(length=4)
# Feed 8 impulses, read delayed output
x = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
for s in x:
print(dl.step(s), end=" ")
# 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0
print()
# Reset clears the buffer but preserves length
dl.reset()
assert dl.get_length() == 4 # length survives reset
assert dl.step(1.0) == 0.0 # fresh delay line
# Batch processing
signal = np.zeros(8, dtype=np.float32)
signal[0] = 1.0
out = DelayLine(length=4).steps(signal)
print(out) # [0. 0. 0. 0. 1. 0. 0. 0.]
Key concepts¶
reset_impl owns the reset logic entirely. When reset_impl is present
the auto-generated field-assignment is skipped. That makes it possible to zero
taps in place without freeing it — the allocation persists across reset(),
keeping length valid.
Opaque fields + scalar fields can coexist. taps is opaque (no
auto-getter, no constructor param); length and idx are scalar and do get
auto-generated getters and constructor params as normal.
Don't call set_length() to resize. The auto-generated setter writes the
scalar field but does not touch the taps allocation. If you need runtime
resize, expose a custom resize method that reallocs the buffer and updates
length and idx atomically. See the pitfall examples in
Declarative scaffolding — opaque state.
See also¶
- opaque_counter example — the minimal "hello world" of opaque fields
- Declarative scaffolding — opaque state fields
- Declarative scaffolding — custom destroy() body