dsp_toolkit example
A two-component DSP library built with just-makeit: a Gain component and
an Ema (exponential moving average) component.
Follow along to scaffold, implement, and combine them — and see the one place the generator currently needs a manual touch when you add a second component.
TL;DR — see it work first
. <(curl -fsSL https://just-buildit.github.io/just-makeit/install.sh)
just-makeit example dsp_toolkit
# dsp_toolkit: PASSED
Prerequisites
. <(curl -fsSL https://just-buildit.github.io/just-makeit/install.sh)
Pass a custom path to keep the venv somewhere persistent:
. <(curl -fsSL https://just-buildit.github.io/just-makeit/install.sh) -- ~/my-venv
Or with pip if just-makeit is already installed:
pip install just-makeit && just-makeit install-deps
source /tmp/jm-venv/bin/activate
1. Scaffold
just-makeit new dsp_toolkit \
--object gain \
--arg-type float \
--return-type float \
--state "gain:float:1.0"
cd dsp_toolkit && make
Scaffolds a single gain component with a real-valued step():
| Name | Type | Default | Role |
|---|---|---|---|
gain |
float |
1.0 |
Scalar gain |
make configures CMake and builds the extension. The C test already passes
before you write a single line of logic.
2. Implement gain
Open native/inc/gain/gain_core.h and replace the gain_step stub — it's a one-liner:
static inline float
gain_step(const gain_state_t *state, float x)
{
return state->gain * x;
}
gain_steps() in gain_core.c loops over this automatically — no changes needed there.
3. Add a second component
just-makeit object ema \
--arg-type float \
--return-type float \
--state "alpha:double:0.1" \
--state "prev:float:0.0"
just-makeit object adds ema alongside gain in the same project:
- new C header, source, test, and benchmark under
native/ - new Python stub, test, and benchmark under
src/dsp_toolkit/ - umbrella header
native/inc/dsp_toolkit.hupdated with#include "ema/ema_core.h" - root
CMakeLists.txtupdated withadd_subdirectoryandtarget_link_libraries
State:
| Name | Type | Default | Role |
|---|---|---|---|
alpha |
double |
0.1 |
Smoothing factor (0 < α < 1) |
prev |
float |
0.0 |
Previous output sample |
4. Implement ema
ema_step must write back to state->prev, so the signature drops const:
static inline float
ema_step(ema_state_t *state, float x)
{
float y = (float)state->alpha * x
+ (float)(1.0 - state->alpha) * state->prev;
state->prev = y;
return y;
}
The patch script handles both the body replacement and the const removal:
"""Patch ema_step stub with the implementation.
EMA writes back to state->prev, so the signature drops `const`:
const ema_state_t * -> ema_state_t *
Run from the project root: python3 .steps/04_patch.py
"""
import pathlib
import re
import sys
header = pathlib.Path("native/inc/ema/ema_core.h")
impl = pathlib.Path(__file__).with_name("04_step_after.c")
stub_re = re.compile(
r"(static inline|JM_FORCEINLINE JM_HOT) float\s*\n"
r"ema_step\((?:const )?ema_state_t \*state.*?\n\}",
re.DOTALL,
)
text = header.read_text()
m = stub_re.search(text)
if not m:
print("ERROR: stub not found — already patched or file changed", file=sys.stderr)
sys.exit(1)
qualifier = m.group(1)
# The impl already has the non-const signature; sub replaces the whole match.
replacement = impl.read_text().strip().replace("static inline", qualifier, 1)
patched = stub_re.sub(replacement, text)
header.write_text(patched)
print(f"patched {header}")
5. Both components are exported automatically
After just-makeit object, __init__.py is updated in-place — the new import
and __all__ entry are spliced in without touching anything else:
"""dsp_toolkit — Gain component."""
from .gain import Gain
from .ema import Ema
__all__ = ["Gain", "Ema"]
Existing imports and any user additions are preserved.
6. Build and test
make && make test
CTest runs test_gain_core and test_ema_core.
pytest runs the 14 generated tests across both components.
7. Use from Python
"""Demo: use Gain and Ema together from Python."""
import sys
sys.path.insert(0, "src")
import numpy as np
from dsp_toolkit import Gain, Ema
# A short burst followed by silence
signal = np.ones(20, dtype=np.float32)
signal[10:] = 0.0
gain = Gain(gain=2.0)
ema = Ema(alpha=0.3)
print(f"{'n':>3} {'input':>7} {'gained':>7} {'smoothed':>10}")
print("-" * 36)
for i, x in enumerate(signal):
y_gain = gain.step(x)
y_ema = ema.step(float(y_gain))
print(f"{i:>3} {x:>7.3f} {y_gain:>7.3f} {y_ema:>10.4f}")
Gain and Ema are independent stateful objects — chain them however you need.
steps() is also available for block processing:
y = Gain(gain=2.0).steps(signal) # returns float32 ndarray
8. Use from Python
"""Demo: use Gain and Ema together from Python."""
import sys
sys.path.insert(0, "src")
import numpy as np
from dsp_toolkit import Gain, Ema
# A short burst followed by silence
signal = np.ones(20, dtype=np.float32)
signal[10:] = 0.0
gain = Gain(gain=2.0)
ema = Ema(alpha=0.3)
print(f"{'n':>3} {'input':>7} {'gained':>7} {'smoothed':>10}")
print("-" * 36)
for i, x in enumerate(signal):
y_gain = gain.step(x)
y_ema = ema.step(float(y_gain))
print(f"{i:>3} {x:>7.3f} {y_gain:>7.3f} {y_ema:>10.4f}")
Gain and Ema are independent stateful objects — chain them however you need.
steps() is also available for block processing:
y = Gain(gain=2.0).steps(signal) # returns float32 ndarray