dsp-graph 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dsp_graph-0.1.5/PKG-INFO +239 -0
- dsp_graph-0.1.5/README.md +208 -0
- dsp_graph-0.1.5/pyproject.toml +68 -0
- dsp_graph-0.1.5/src/dsp_graph/__init__.py +118 -0
- dsp_graph-0.1.5/src/dsp_graph/_deps.py +37 -0
- dsp_graph-0.1.5/src/dsp_graph/compile.py +1084 -0
- dsp_graph-0.1.5/src/dsp_graph/gen_dsp_adapter.py +305 -0
- dsp_graph-0.1.5/src/dsp_graph/models.py +364 -0
- dsp_graph-0.1.5/src/dsp_graph/optimize.py +393 -0
- dsp_graph-0.1.5/src/dsp_graph/py.typed +0 -0
- dsp_graph-0.1.5/src/dsp_graph/toposort.py +61 -0
- dsp_graph-0.1.5/src/dsp_graph/validate.py +121 -0
- dsp_graph-0.1.5/src/dsp_graph/visualize.py +211 -0
dsp_graph-0.1.5/PKG-INFO
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: dsp-graph
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: A Python DSL for defining DSP signal graphs with C++ compilation and optimization
|
|
5
|
+
Keywords: dsp,audio,signal-processing,codegen,c++,pydantic,graph
|
|
6
|
+
Author: Shakeeb Alireza
|
|
7
|
+
Author-email: Shakeeb Alireza <shakfu@users.noreply.github.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Other Audience
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
23
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Project-URL: Homepage, https://github.com/shakfu/dsp-graph
|
|
27
|
+
Project-URL: Repository, https://github.com/shakfu/dsp-graph
|
|
28
|
+
Project-URL: Changelog, https://github.com/shakfu/dsp-graph/blob/main/CHANGELOG.md
|
|
29
|
+
Project-URL: Issues, https://github.com/shakfu/dsp-graph/issues
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# dsp-graph
|
|
33
|
+
|
|
34
|
+
A Python DSL for defining DSP signal graphs, compiling them to standalone C++, and optimizing the result.
|
|
35
|
+
|
|
36
|
+
Define audio processing graphs using Pydantic models, validate them, compile to C++, and serialize to/from JSON. Zero runtime dependencies beyond Pydantic.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install dsp-graph
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For development:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/shakfu/dsp-graph.git
|
|
48
|
+
cd dsp-graph
|
|
49
|
+
make install-dev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from dsp_graph import (
|
|
56
|
+
AudioInput, AudioOutput, BinOp, Graph, History, Param,
|
|
57
|
+
compile_graph, validate_graph,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
graph = Graph(
|
|
61
|
+
name="onepole",
|
|
62
|
+
inputs=[AudioInput(id="in1")],
|
|
63
|
+
outputs=[AudioOutput(id="out1", source="result")],
|
|
64
|
+
params=[Param(name="coeff", min=0.0, max=0.999, default=0.5)],
|
|
65
|
+
nodes=[
|
|
66
|
+
BinOp(id="inv_coeff", op="sub", a=1.0, b="coeff"),
|
|
67
|
+
BinOp(id="dry", op="mul", a="in1", b="inv_coeff"),
|
|
68
|
+
History(id="prev", init=0.0, input="result"),
|
|
69
|
+
BinOp(id="wet", op="mul", a="prev", b="coeff"),
|
|
70
|
+
BinOp(id="result", op="add", a="dry", b="wet"),
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
errors = validate_graph(graph)
|
|
75
|
+
assert errors == []
|
|
76
|
+
|
|
77
|
+
code = compile_graph(graph) # standalone C++ string
|
|
78
|
+
print(graph.model_dump_json(indent=2))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Node Types (34)
|
|
82
|
+
|
|
83
|
+
### Arithmetic / Math
|
|
84
|
+
|
|
85
|
+
| Node | `op` | Fields | Purpose |
|
|
86
|
+
|------|------|--------|---------|
|
|
87
|
+
| `BinOp` | `add`, `sub`, `mul`, `div`, `min`, `max`, `mod`, `pow` | `a`, `b` | Binary arithmetic |
|
|
88
|
+
| `UnaryOp` | `sin`, `cos`, `tanh`, `exp`, `log`, `abs`, `sqrt`, `neg`, `floor`, `ceil`, `round`, `sign`, `atan`, `asin`, `acos` | `a` | Unary math functions |
|
|
89
|
+
| `Clamp` | `clamp` | `a`, `lo`, `hi` | Saturate to `[lo, hi]` |
|
|
90
|
+
| `Constant` | `constant` | `value` | Literal float value |
|
|
91
|
+
| `Compare` | `gt`, `lt`, `gte`, `lte`, `eq` | `a`, `b` | Comparison (returns 0.0 or 1.0) |
|
|
92
|
+
| `Select` | `select` | `cond`, `a`, `b` | Conditional: `a` if `cond > 0`, else `b` |
|
|
93
|
+
| `Wrap` | `wrap` | `a`, `lo`, `hi` | Wrap value into range |
|
|
94
|
+
| `Fold` | `fold` | `a`, `lo`, `hi` | Fold (reflect) value into range |
|
|
95
|
+
| `Mix` | `mix` | `a`, `b`, `t` | Linear interpolation: `a + (b - a) * t` |
|
|
96
|
+
|
|
97
|
+
### Delay
|
|
98
|
+
|
|
99
|
+
| Node | `op` | Fields | Purpose |
|
|
100
|
+
|------|------|--------|---------|
|
|
101
|
+
| `DelayLine` | `delay` | `max_samples` | Circular buffer declaration |
|
|
102
|
+
| `DelayRead` | `delay_read` | `delay`, `tap`, `interp` | Read from delay line (none/linear/cubic) |
|
|
103
|
+
| `DelayWrite` | `delay_write` | `delay`, `value` | Write to delay line |
|
|
104
|
+
| `History` | `history` | `input`, `init` | Single-sample delay (z^-1 feedback) |
|
|
105
|
+
|
|
106
|
+
### Buffer / Table
|
|
107
|
+
|
|
108
|
+
| Node | `op` | Fields | Purpose |
|
|
109
|
+
|------|------|--------|---------|
|
|
110
|
+
| `Buffer` | `buffer` | `size` | Random-access data buffer |
|
|
111
|
+
| `BufRead` | `buf_read` | `buffer`, `index`, `interp` | Read from buffer (none/linear/cubic, clamped) |
|
|
112
|
+
| `BufWrite` | `buf_write` | `buffer`, `index`, `value` | Write to buffer at index |
|
|
113
|
+
| `BufSize` | `buf_size` | `buffer` | Returns buffer length as float |
|
|
114
|
+
|
|
115
|
+
### Filters
|
|
116
|
+
|
|
117
|
+
| Node | `op` | Fields | Purpose |
|
|
118
|
+
|------|------|--------|---------|
|
|
119
|
+
| `Biquad` | `biquad` | `a`, `b0`, `b1`, `b2`, `a1`, `a2` | Generic biquad (user supplies coefficients) |
|
|
120
|
+
| `SVF` | `svf` | `a`, `freq`, `q`, `mode` | State-variable filter (lp/hp/bp/notch) |
|
|
121
|
+
| `OnePole` | `onepole` | `a`, `coeff` | One-pole lowpass |
|
|
122
|
+
| `DCBlock` | `dcblock` | `a` | DC blocking filter |
|
|
123
|
+
| `Allpass` | `allpass` | `a`, `coeff` | First-order allpass |
|
|
124
|
+
|
|
125
|
+
### Oscillators / Sources
|
|
126
|
+
|
|
127
|
+
| Node | `op` | Fields | Purpose |
|
|
128
|
+
|------|------|--------|---------|
|
|
129
|
+
| `Phasor` | `phasor` | `freq` | Ramp oscillator 0..1 |
|
|
130
|
+
| `SinOsc` | `sinosc` | `freq` | Sine oscillator |
|
|
131
|
+
| `TriOsc` | `triosc` | `freq` | Triangle wave |
|
|
132
|
+
| `SawOsc` | `sawosc` | `freq` | Bipolar saw (-1..1) |
|
|
133
|
+
| `PulseOsc` | `pulseosc` | `freq`, `width` | Pulse/square with variable duty cycle |
|
|
134
|
+
| `Noise` | `noise` | -- | White noise source |
|
|
135
|
+
|
|
136
|
+
### State / Timing
|
|
137
|
+
|
|
138
|
+
| Node | `op` | Fields | Purpose |
|
|
139
|
+
|------|------|--------|---------|
|
|
140
|
+
| `Delta` | `delta` | `a` | Sample-to-sample difference |
|
|
141
|
+
| `Change` | `change` | `a` | 1.0 if value changed, else 0.0 |
|
|
142
|
+
| `SampleHold` | `sample_hold` | `a`, `trig` | Latch on any zero crossing |
|
|
143
|
+
| `Latch` | `latch` | `a`, `trig` | Latch on rising edge only |
|
|
144
|
+
| `Accum` | `accum` | `incr`, `reset` | Running sum, resets when `reset > 0` |
|
|
145
|
+
| `Counter` | `counter` | `trig`, `max` | Integer counter, wraps at max |
|
|
146
|
+
|
|
147
|
+
## C++ Compilation
|
|
148
|
+
|
|
149
|
+
`compile_graph()` generates a single self-contained `.cpp` file with:
|
|
150
|
+
|
|
151
|
+
- A state struct (`{Name}State`)
|
|
152
|
+
- `create(sr)` / `destroy(self)` / `reset(self)` lifecycle
|
|
153
|
+
- `perform(self, ins, outs, n)` sample-processing loop
|
|
154
|
+
- Param introspection: `num_params`, `param_name`, `param_min`, `param_max`, `set_param`, `get_param`
|
|
155
|
+
- Buffer introspection: `num_buffers`, `buffer_name`, `buffer_size`, `get_buffer`, `set_buffer`
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from dsp_graph import compile_graph, compile_graph_to_file
|
|
159
|
+
|
|
160
|
+
code = compile_graph(graph) # returns C++ string
|
|
161
|
+
path = compile_graph_to_file(graph, "build/") # writes build/{name}.cpp
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## gen-dsp Integration
|
|
165
|
+
|
|
166
|
+
dsp-graph graphs can be compiled into buildable audio plugin projects via [gen-dsp](https://github.com/shakfu/gen-dsp), which supports 11 platforms: ChucK, CLAP, AudioUnit, VST3, LV2, SuperCollider, VCV Rack, Daisy, and more.
|
|
167
|
+
|
|
168
|
+
`compile_for_gen_dsp()` generates the three files needed to drop into any gen-dsp platform backend:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from dsp_graph import compile_for_gen_dsp
|
|
172
|
+
|
|
173
|
+
# Generates: test_synth.cpp, _ext_chuck.cpp, manifest.json
|
|
174
|
+
compile_for_gen_dsp(graph, "build/", platform="chuck")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The adapter replaces gen-dsp's genlib-side code while reusing its platform-side code unchanged. The generated `manifest.json` is compatible with `gen_dsp.core.manifest.Manifest`.
|
|
178
|
+
|
|
179
|
+
For a fully assembled project (requires gen-dsp installed):
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from dsp_graph.gen_dsp_adapter import assemble_project
|
|
183
|
+
|
|
184
|
+
# Copies platform templates + generates adapter + manifest
|
|
185
|
+
assemble_project(graph, "build/chuck_project", platform="chuck")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Optimization
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from dsp_graph import optimize_graph, constant_fold, eliminate_cse, eliminate_dead_nodes
|
|
192
|
+
|
|
193
|
+
optimized = optimize_graph(graph) # constant folding + CSE + dead node elimination
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- **Constant folding**: pure nodes with all-constant inputs are replaced by `Constant` nodes
|
|
197
|
+
- **Common subexpression elimination**: duplicate pure nodes with identical inputs are merged
|
|
198
|
+
- **Dead node elimination**: nodes not reachable from any output are removed (respects side-effecting writers for delay lines and buffers)
|
|
199
|
+
- **Loop-invariant code motion**: param-only expressions are hoisted before the sample loop
|
|
200
|
+
- **SIMD hints**: `__restrict` on I/O pointers; vectorization pragmas for pure-only graphs
|
|
201
|
+
|
|
202
|
+
## Validation
|
|
203
|
+
|
|
204
|
+
`validate_graph()` checks:
|
|
205
|
+
|
|
206
|
+
1. Unique node IDs (no collisions with inputs or params)
|
|
207
|
+
2. All string references resolve to existing IDs
|
|
208
|
+
3. Output sources reference existing nodes
|
|
209
|
+
4. DelayRead/DelayWrite reference existing DelayLine nodes
|
|
210
|
+
5. BufRead/BufWrite/BufSize reference existing Buffer nodes
|
|
211
|
+
6. No pure cycles (cycles must pass through History or delay)
|
|
212
|
+
|
|
213
|
+
## Visualization
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from dsp_graph import graph_to_dot, graph_to_dot_file
|
|
217
|
+
|
|
218
|
+
dot_str = graph_to_dot(graph) # DOT string
|
|
219
|
+
dot_path = graph_to_dot_file(graph, "build/") # writes .dot, renders .pdf if `dot` is on PATH
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Examples
|
|
223
|
+
|
|
224
|
+
See `examples/` for complete working graphs:
|
|
225
|
+
|
|
226
|
+
- `stereo_gain.py` -- stateless stereo gain
|
|
227
|
+
- `onepole.py` -- one-pole lowpass with History feedback
|
|
228
|
+
- `fbdelay.py` -- feedback delay with dry/wet mix
|
|
229
|
+
- `wavetable.py` -- wavetable oscillator using Buffer + Phasor + BufRead
|
|
230
|
+
|
|
231
|
+
## Development
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
make install-dev # install with dev deps
|
|
235
|
+
make test # run tests
|
|
236
|
+
make lint # ruff check
|
|
237
|
+
make typecheck # mypy --strict
|
|
238
|
+
make qa # all of the above
|
|
239
|
+
```
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# dsp-graph
|
|
2
|
+
|
|
3
|
+
A Python DSL for defining DSP signal graphs, compiling them to standalone C++, and optimizing the result.
|
|
4
|
+
|
|
5
|
+
Define audio processing graphs using Pydantic models, validate them, compile to C++, and serialize to/from JSON. Zero runtime dependencies beyond Pydantic.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install dsp-graph
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For development:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git clone https://github.com/shakfu/dsp-graph.git
|
|
17
|
+
cd dsp-graph
|
|
18
|
+
make install-dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from dsp_graph import (
|
|
25
|
+
AudioInput, AudioOutput, BinOp, Graph, History, Param,
|
|
26
|
+
compile_graph, validate_graph,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
graph = Graph(
|
|
30
|
+
name="onepole",
|
|
31
|
+
inputs=[AudioInput(id="in1")],
|
|
32
|
+
outputs=[AudioOutput(id="out1", source="result")],
|
|
33
|
+
params=[Param(name="coeff", min=0.0, max=0.999, default=0.5)],
|
|
34
|
+
nodes=[
|
|
35
|
+
BinOp(id="inv_coeff", op="sub", a=1.0, b="coeff"),
|
|
36
|
+
BinOp(id="dry", op="mul", a="in1", b="inv_coeff"),
|
|
37
|
+
History(id="prev", init=0.0, input="result"),
|
|
38
|
+
BinOp(id="wet", op="mul", a="prev", b="coeff"),
|
|
39
|
+
BinOp(id="result", op="add", a="dry", b="wet"),
|
|
40
|
+
],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
errors = validate_graph(graph)
|
|
44
|
+
assert errors == []
|
|
45
|
+
|
|
46
|
+
code = compile_graph(graph) # standalone C++ string
|
|
47
|
+
print(graph.model_dump_json(indent=2))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Node Types (34)
|
|
51
|
+
|
|
52
|
+
### Arithmetic / Math
|
|
53
|
+
|
|
54
|
+
| Node | `op` | Fields | Purpose |
|
|
55
|
+
|------|------|--------|---------|
|
|
56
|
+
| `BinOp` | `add`, `sub`, `mul`, `div`, `min`, `max`, `mod`, `pow` | `a`, `b` | Binary arithmetic |
|
|
57
|
+
| `UnaryOp` | `sin`, `cos`, `tanh`, `exp`, `log`, `abs`, `sqrt`, `neg`, `floor`, `ceil`, `round`, `sign`, `atan`, `asin`, `acos` | `a` | Unary math functions |
|
|
58
|
+
| `Clamp` | `clamp` | `a`, `lo`, `hi` | Saturate to `[lo, hi]` |
|
|
59
|
+
| `Constant` | `constant` | `value` | Literal float value |
|
|
60
|
+
| `Compare` | `gt`, `lt`, `gte`, `lte`, `eq` | `a`, `b` | Comparison (returns 0.0 or 1.0) |
|
|
61
|
+
| `Select` | `select` | `cond`, `a`, `b` | Conditional: `a` if `cond > 0`, else `b` |
|
|
62
|
+
| `Wrap` | `wrap` | `a`, `lo`, `hi` | Wrap value into range |
|
|
63
|
+
| `Fold` | `fold` | `a`, `lo`, `hi` | Fold (reflect) value into range |
|
|
64
|
+
| `Mix` | `mix` | `a`, `b`, `t` | Linear interpolation: `a + (b - a) * t` |
|
|
65
|
+
|
|
66
|
+
### Delay
|
|
67
|
+
|
|
68
|
+
| Node | `op` | Fields | Purpose |
|
|
69
|
+
|------|------|--------|---------|
|
|
70
|
+
| `DelayLine` | `delay` | `max_samples` | Circular buffer declaration |
|
|
71
|
+
| `DelayRead` | `delay_read` | `delay`, `tap`, `interp` | Read from delay line (none/linear/cubic) |
|
|
72
|
+
| `DelayWrite` | `delay_write` | `delay`, `value` | Write to delay line |
|
|
73
|
+
| `History` | `history` | `input`, `init` | Single-sample delay (z^-1 feedback) |
|
|
74
|
+
|
|
75
|
+
### Buffer / Table
|
|
76
|
+
|
|
77
|
+
| Node | `op` | Fields | Purpose |
|
|
78
|
+
|------|------|--------|---------|
|
|
79
|
+
| `Buffer` | `buffer` | `size` | Random-access data buffer |
|
|
80
|
+
| `BufRead` | `buf_read` | `buffer`, `index`, `interp` | Read from buffer (none/linear/cubic, clamped) |
|
|
81
|
+
| `BufWrite` | `buf_write` | `buffer`, `index`, `value` | Write to buffer at index |
|
|
82
|
+
| `BufSize` | `buf_size` | `buffer` | Returns buffer length as float |
|
|
83
|
+
|
|
84
|
+
### Filters
|
|
85
|
+
|
|
86
|
+
| Node | `op` | Fields | Purpose |
|
|
87
|
+
|------|------|--------|---------|
|
|
88
|
+
| `Biquad` | `biquad` | `a`, `b0`, `b1`, `b2`, `a1`, `a2` | Generic biquad (user supplies coefficients) |
|
|
89
|
+
| `SVF` | `svf` | `a`, `freq`, `q`, `mode` | State-variable filter (lp/hp/bp/notch) |
|
|
90
|
+
| `OnePole` | `onepole` | `a`, `coeff` | One-pole lowpass |
|
|
91
|
+
| `DCBlock` | `dcblock` | `a` | DC blocking filter |
|
|
92
|
+
| `Allpass` | `allpass` | `a`, `coeff` | First-order allpass |
|
|
93
|
+
|
|
94
|
+
### Oscillators / Sources
|
|
95
|
+
|
|
96
|
+
| Node | `op` | Fields | Purpose |
|
|
97
|
+
|------|------|--------|---------|
|
|
98
|
+
| `Phasor` | `phasor` | `freq` | Ramp oscillator 0..1 |
|
|
99
|
+
| `SinOsc` | `sinosc` | `freq` | Sine oscillator |
|
|
100
|
+
| `TriOsc` | `triosc` | `freq` | Triangle wave |
|
|
101
|
+
| `SawOsc` | `sawosc` | `freq` | Bipolar saw (-1..1) |
|
|
102
|
+
| `PulseOsc` | `pulseosc` | `freq`, `width` | Pulse/square with variable duty cycle |
|
|
103
|
+
| `Noise` | `noise` | -- | White noise source |
|
|
104
|
+
|
|
105
|
+
### State / Timing
|
|
106
|
+
|
|
107
|
+
| Node | `op` | Fields | Purpose |
|
|
108
|
+
|------|------|--------|---------|
|
|
109
|
+
| `Delta` | `delta` | `a` | Sample-to-sample difference |
|
|
110
|
+
| `Change` | `change` | `a` | 1.0 if value changed, else 0.0 |
|
|
111
|
+
| `SampleHold` | `sample_hold` | `a`, `trig` | Latch on any zero crossing |
|
|
112
|
+
| `Latch` | `latch` | `a`, `trig` | Latch on rising edge only |
|
|
113
|
+
| `Accum` | `accum` | `incr`, `reset` | Running sum, resets when `reset > 0` |
|
|
114
|
+
| `Counter` | `counter` | `trig`, `max` | Integer counter, wraps at max |
|
|
115
|
+
|
|
116
|
+
## C++ Compilation
|
|
117
|
+
|
|
118
|
+
`compile_graph()` generates a single self-contained `.cpp` file with:
|
|
119
|
+
|
|
120
|
+
- A state struct (`{Name}State`)
|
|
121
|
+
- `create(sr)` / `destroy(self)` / `reset(self)` lifecycle
|
|
122
|
+
- `perform(self, ins, outs, n)` sample-processing loop
|
|
123
|
+
- Param introspection: `num_params`, `param_name`, `param_min`, `param_max`, `set_param`, `get_param`
|
|
124
|
+
- Buffer introspection: `num_buffers`, `buffer_name`, `buffer_size`, `get_buffer`, `set_buffer`
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from dsp_graph import compile_graph, compile_graph_to_file
|
|
128
|
+
|
|
129
|
+
code = compile_graph(graph) # returns C++ string
|
|
130
|
+
path = compile_graph_to_file(graph, "build/") # writes build/{name}.cpp
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## gen-dsp Integration
|
|
134
|
+
|
|
135
|
+
dsp-graph graphs can be compiled into buildable audio plugin projects via [gen-dsp](https://github.com/shakfu/gen-dsp), which supports 11 platforms: ChucK, CLAP, AudioUnit, VST3, LV2, SuperCollider, VCV Rack, Daisy, and more.
|
|
136
|
+
|
|
137
|
+
`compile_for_gen_dsp()` generates the three files needed to drop into any gen-dsp platform backend:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from dsp_graph import compile_for_gen_dsp
|
|
141
|
+
|
|
142
|
+
# Generates: test_synth.cpp, _ext_chuck.cpp, manifest.json
|
|
143
|
+
compile_for_gen_dsp(graph, "build/", platform="chuck")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The adapter replaces gen-dsp's genlib-side code while reusing its platform-side code unchanged. The generated `manifest.json` is compatible with `gen_dsp.core.manifest.Manifest`.
|
|
147
|
+
|
|
148
|
+
For a fully assembled project (requires gen-dsp installed):
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from dsp_graph.gen_dsp_adapter import assemble_project
|
|
152
|
+
|
|
153
|
+
# Copies platform templates + generates adapter + manifest
|
|
154
|
+
assemble_project(graph, "build/chuck_project", platform="chuck")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Optimization
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from dsp_graph import optimize_graph, constant_fold, eliminate_cse, eliminate_dead_nodes
|
|
161
|
+
|
|
162
|
+
optimized = optimize_graph(graph) # constant folding + CSE + dead node elimination
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- **Constant folding**: pure nodes with all-constant inputs are replaced by `Constant` nodes
|
|
166
|
+
- **Common subexpression elimination**: duplicate pure nodes with identical inputs are merged
|
|
167
|
+
- **Dead node elimination**: nodes not reachable from any output are removed (respects side-effecting writers for delay lines and buffers)
|
|
168
|
+
- **Loop-invariant code motion**: param-only expressions are hoisted before the sample loop
|
|
169
|
+
- **SIMD hints**: `__restrict` on I/O pointers; vectorization pragmas for pure-only graphs
|
|
170
|
+
|
|
171
|
+
## Validation
|
|
172
|
+
|
|
173
|
+
`validate_graph()` checks:
|
|
174
|
+
|
|
175
|
+
1. Unique node IDs (no collisions with inputs or params)
|
|
176
|
+
2. All string references resolve to existing IDs
|
|
177
|
+
3. Output sources reference existing nodes
|
|
178
|
+
4. DelayRead/DelayWrite reference existing DelayLine nodes
|
|
179
|
+
5. BufRead/BufWrite/BufSize reference existing Buffer nodes
|
|
180
|
+
6. No pure cycles (cycles must pass through History or delay)
|
|
181
|
+
|
|
182
|
+
## Visualization
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from dsp_graph import graph_to_dot, graph_to_dot_file
|
|
186
|
+
|
|
187
|
+
dot_str = graph_to_dot(graph) # DOT string
|
|
188
|
+
dot_path = graph_to_dot_file(graph, "build/") # writes .dot, renders .pdf if `dot` is on PATH
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Examples
|
|
192
|
+
|
|
193
|
+
See `examples/` for complete working graphs:
|
|
194
|
+
|
|
195
|
+
- `stereo_gain.py` -- stateless stereo gain
|
|
196
|
+
- `onepole.py` -- one-pole lowpass with History feedback
|
|
197
|
+
- `fbdelay.py` -- feedback delay with dry/wet mix
|
|
198
|
+
- `wavetable.py` -- wavetable oscillator using Buffer + Phasor + BufRead
|
|
199
|
+
|
|
200
|
+
## Development
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
make install-dev # install with dev deps
|
|
204
|
+
make test # run tests
|
|
205
|
+
make lint # ruff check
|
|
206
|
+
make typecheck # mypy --strict
|
|
207
|
+
make qa # all of the above
|
|
208
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dsp-graph"
|
|
3
|
+
version = "0.1.5"
|
|
4
|
+
description = "A Python DSL for defining DSP signal graphs with C++ compilation and optimization"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "MIT"}
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Shakeeb Alireza", email = "shakfu@users.noreply.github.com" }
|
|
9
|
+
]
|
|
10
|
+
keywords = ["dsp", "audio", "signal-processing", "codegen", "c++", "pydantic", "graph"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Intended Audience :: Other Audience",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: MacOS",
|
|
18
|
+
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Multimedia :: Sound/Audio",
|
|
26
|
+
"Topic :: Software Development :: Code Generators",
|
|
27
|
+
]
|
|
28
|
+
requires-python = ">=3.10"
|
|
29
|
+
dependencies = [
|
|
30
|
+
"pydantic>=2.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"gen-dsp>=0.1.6",
|
|
36
|
+
"mypy>=1.19.1",
|
|
37
|
+
"pytest>=9.0.2",
|
|
38
|
+
"pytest-cov>=7.0.0",
|
|
39
|
+
"ruff>=0.15.0",
|
|
40
|
+
"twine>=6.2.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/shakfu/dsp-graph"
|
|
45
|
+
Repository = "https://github.com/shakfu/dsp-graph"
|
|
46
|
+
Changelog = "https://github.com/shakfu/dsp-graph/blob/main/CHANGELOG.md"
|
|
47
|
+
Issues = "https://github.com/shakfu/dsp-graph/issues"
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["uv_build>=0.10.2,<0.11.0"]
|
|
51
|
+
build-backend = "uv_build"
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
addopts = "--cov=dsp_graph --cov-report term-missing:skip-covered"
|
|
56
|
+
|
|
57
|
+
[tool.mypy]
|
|
58
|
+
strict = true
|
|
59
|
+
packages = ["dsp_graph"]
|
|
60
|
+
mypy_path = "src"
|
|
61
|
+
plugins = ["pydantic.mypy"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
src = ["src"]
|
|
65
|
+
line-length = 99
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint]
|
|
68
|
+
select = ["E", "F", "I", "W"]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Define DSP signal graphs as Pydantic models, compile to C++, and optimize.
|
|
2
|
+
|
|
3
|
+
Provides 34 node types (arithmetic, filters, oscillators, delays, buffers,
|
|
4
|
+
state/timing), graph validation, topological sort, Graphviz visualization,
|
|
5
|
+
and a multi-pass optimizing compiler targeting standalone C++. Optional
|
|
6
|
+
gen-dsp integration generates adapter code for 10+ audio plugin platforms.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dsp_graph.compile import compile_graph, compile_graph_to_file
|
|
10
|
+
from dsp_graph.gen_dsp_adapter import (
|
|
11
|
+
compile_for_gen_dsp,
|
|
12
|
+
generate_adapter_cpp,
|
|
13
|
+
generate_manifest,
|
|
14
|
+
)
|
|
15
|
+
from dsp_graph.models import (
|
|
16
|
+
SVF,
|
|
17
|
+
Accum,
|
|
18
|
+
Allpass,
|
|
19
|
+
AudioInput,
|
|
20
|
+
AudioOutput,
|
|
21
|
+
BinOp,
|
|
22
|
+
Biquad,
|
|
23
|
+
Buffer,
|
|
24
|
+
BufRead,
|
|
25
|
+
BufSize,
|
|
26
|
+
BufWrite,
|
|
27
|
+
Change,
|
|
28
|
+
Clamp,
|
|
29
|
+
Compare,
|
|
30
|
+
Constant,
|
|
31
|
+
Counter,
|
|
32
|
+
DCBlock,
|
|
33
|
+
DelayLine,
|
|
34
|
+
DelayRead,
|
|
35
|
+
DelayWrite,
|
|
36
|
+
Delta,
|
|
37
|
+
Fold,
|
|
38
|
+
Graph,
|
|
39
|
+
History,
|
|
40
|
+
Latch,
|
|
41
|
+
Mix,
|
|
42
|
+
Node,
|
|
43
|
+
Noise,
|
|
44
|
+
OnePole,
|
|
45
|
+
Param,
|
|
46
|
+
Phasor,
|
|
47
|
+
PulseOsc,
|
|
48
|
+
Ref,
|
|
49
|
+
SampleHold,
|
|
50
|
+
SawOsc,
|
|
51
|
+
Select,
|
|
52
|
+
SinOsc,
|
|
53
|
+
TriOsc,
|
|
54
|
+
UnaryOp,
|
|
55
|
+
Wrap,
|
|
56
|
+
)
|
|
57
|
+
from dsp_graph.optimize import constant_fold, eliminate_cse, eliminate_dead_nodes, optimize_graph
|
|
58
|
+
from dsp_graph.toposort import toposort
|
|
59
|
+
from dsp_graph.validate import validate_graph
|
|
60
|
+
from dsp_graph.visualize import graph_to_dot, graph_to_dot_file
|
|
61
|
+
|
|
62
|
+
__version__ = "0.1.5"
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"Accum",
|
|
66
|
+
"Allpass",
|
|
67
|
+
"AudioInput",
|
|
68
|
+
"AudioOutput",
|
|
69
|
+
"BinOp",
|
|
70
|
+
"Biquad",
|
|
71
|
+
"BufRead",
|
|
72
|
+
"BufSize",
|
|
73
|
+
"BufWrite",
|
|
74
|
+
"Buffer",
|
|
75
|
+
"Change",
|
|
76
|
+
"Clamp",
|
|
77
|
+
"Compare",
|
|
78
|
+
"Constant",
|
|
79
|
+
"Counter",
|
|
80
|
+
"DCBlock",
|
|
81
|
+
"DelayLine",
|
|
82
|
+
"DelayRead",
|
|
83
|
+
"DelayWrite",
|
|
84
|
+
"Delta",
|
|
85
|
+
"Fold",
|
|
86
|
+
"Graph",
|
|
87
|
+
"History",
|
|
88
|
+
"Latch",
|
|
89
|
+
"Mix",
|
|
90
|
+
"Node",
|
|
91
|
+
"Noise",
|
|
92
|
+
"OnePole",
|
|
93
|
+
"Param",
|
|
94
|
+
"Phasor",
|
|
95
|
+
"PulseOsc",
|
|
96
|
+
"Ref",
|
|
97
|
+
"SVF",
|
|
98
|
+
"SampleHold",
|
|
99
|
+
"SawOsc",
|
|
100
|
+
"Select",
|
|
101
|
+
"SinOsc",
|
|
102
|
+
"TriOsc",
|
|
103
|
+
"UnaryOp",
|
|
104
|
+
"Wrap",
|
|
105
|
+
"compile_for_gen_dsp",
|
|
106
|
+
"compile_graph",
|
|
107
|
+
"compile_graph_to_file",
|
|
108
|
+
"constant_fold",
|
|
109
|
+
"eliminate_cse",
|
|
110
|
+
"eliminate_dead_nodes",
|
|
111
|
+
"generate_adapter_cpp",
|
|
112
|
+
"generate_manifest",
|
|
113
|
+
"graph_to_dot",
|
|
114
|
+
"graph_to_dot_file",
|
|
115
|
+
"optimize_graph",
|
|
116
|
+
"toposort",
|
|
117
|
+
"validate_graph",
|
|
118
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Shared dependency helpers for graph analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
from dsp_graph.models import Graph, History
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_feedback_edge(node: object, field_name: str) -> bool:
|
|
11
|
+
"""Return True if a field on this node is a feedback edge (not a data dependency)."""
|
|
12
|
+
# History.input is written at end of sample -- reads previous value
|
|
13
|
+
if isinstance(node, History) and field_name == "input":
|
|
14
|
+
return True
|
|
15
|
+
# DelayRead reads from the delay line written by DelayWrite -- implicit feedback
|
|
16
|
+
# DelayWrite.value IS a data dependency (need the value this sample)
|
|
17
|
+
# But DelayRead.delay and DelayWrite.delay reference a DelayLine, not a data flow
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_forward_deps(graph: Graph) -> dict[str, set[str]]:
|
|
22
|
+
"""Build forward dependency map: {node_id: set of node_ids it depends on}.
|
|
23
|
+
|
|
24
|
+
Excludes feedback edges (History.input) and non-node references
|
|
25
|
+
(audio inputs, param names).
|
|
26
|
+
"""
|
|
27
|
+
node_ids = {node.id for node in graph.nodes}
|
|
28
|
+
deps: dict[str, set[str]] = defaultdict(set)
|
|
29
|
+
for node in graph.nodes:
|
|
30
|
+
nid = node.id
|
|
31
|
+
for field_name, value in node.__dict__.items():
|
|
32
|
+
if field_name in ("id", "op"):
|
|
33
|
+
continue
|
|
34
|
+
if isinstance(value, str) and value in node_ids:
|
|
35
|
+
if not is_feedback_edge(node, field_name):
|
|
36
|
+
deps[nid].add(value)
|
|
37
|
+
return deps
|