mdsp
Python audio DSP primitives with kernels written in Mojo.
All processing uses planar [channels, frames] float32 buffers. Each primitive processes a whole buffer in one Mojo call, with the GIL released. Python chains primitives.
import mdsp
buf = mdsp.read_wav("drums.wav")
chain = mdsp.Chain(
mdsp.Svf("lowpass", cutoff=800.0, q=4.0, channels=buf.channels, sample_rate=buf.sample_rate),
mdsp.Gain(0.5, channels=buf.channels, sample_rate=buf.sample_rate),
)
mdsp.write_wav("out.wav", chain.process(buf), fmt="int24")
Primitives
| Kind | Classes |
|---|---|
| Generators | Phasor, Sine, Saw, Square (PolyBLEP band-limited), Noise (white or pink) |
| Envelopes | Adsr |
| Filters | OnePole, Svf, Biquad (lowpass, highpass, bandpass, notch, low/high shelf, peaking) |
| Delays | Delay (fractional, modulated, feedback, mix), Chorus (flanging and chorus) |
| Dynamics | Compressor (sidechain, soft knee), Limiter |
| Effects | Shaper (tanh, soft, hard), Reverb (Freeverb-style) |
| Stereo | Pan (constant power), Width |
| Ops | Gain |
| Ops | Scale (map [-1, 1] onto a parameter range), Mix (sum up to four inputs) |
| Composition | Chain, Graph |
| Files | read_wav, write_wav |
| Real time | Stream, output_devices |
read_wav and write_wav handle PCM 8/16/24/32 and float 32/64 WAV files with no dependencies. For other formats, read with soundfile and wrap the samples in an AudioBuffer.
Svf is a topology-preserving state-variable filter; modulate it rather than Biquad, which overshoots when its coefficients change fast.
Modulation
A unit's inputs lists the parameters that accept a buffer instead of a fixed value. Pass one by name; it must match the audio in sample rate, channels and frames.
lfo = mdsp.Sine(0.5).generate(48000)
cutoff = mdsp.AudioBuffer(2000 + 1500 * lfo.data, 48000)
swept = mdsp.Svf("lowpass", q=4.0).process(tone, cutoff=cutoff)
Parameter changes ramp over 10 ms, so they do not click. reset() applies the target at once.
Graphs
A Graph wires units together and renders them entirely in Mojo, one call per block instead of one per unit. Nodes are opaque handles. A plain connection must run forward, from a node added earlier to one added later; a connection with a delay may run any direction, including a node to itself, which is how feedback is built.
g = mdsp.Graph(sample_rate=48000.0, channels=2)
src = g.add(mdsp.Input) # the buffer passed to process()
lfo = g.add(mdsp.Sine, freq=0.5)
sweep = g.add(mdsp.Scale, lo=300.0, hi=4000.0, curve="exponential")
filt = g.add(mdsp.Svf, mode="lowpass", cutoff=800.0, q=4.0)
g.connect(lfo, sweep)
g.connect(src, filt)
g.connect(sweep, filt, "cutoff")
g.output = filt
out = g.process(buf) # or g.generate(frames) with no Input node
g.set(filt, "q", 8.0) # parameters change through the graph
process, generate and Chain.process take out= to write into a buffer you already own, which avoids allocating per call. With it, a 64-frame block through a 4-node graph costs 2.2 us against a 1333 us real-time deadline.
Against the same three units in a Chain, a graph is 3.2x faster on 64-frame blocks and produces identical samples.
echo = g.add(mdsp.Mix, gain=1.0, gain2=0.6)
g.connect(tap, echo, "in2", delay=4800) # feedback, 100 ms around the loop
A feedback delay counts samples, so a patch sounds the same whatever block size renders it. A loop shorter than block makes the graph render in smaller chunks, which costs speed but keeps the loop exact.
remove(node) empties a node's slot and drops its connections. Other handles keep working, because nothing is renumbered.
Most units are mono and run once per channel with their own state. Pan and Width see every channel at once, which is what panning and stereo width need.
Units are stateful: state carries across process / generate calls until reset(). An instance is not thread-safe; separate instances run in parallel threads.
Importing mdsp starts the Mojo runtime's idle worker pool: one thread per CPU in the process affinity mask. Start Python under taskset to limit it.
Examples
examples/offline_process.py renders a file through a swept filter and delay, synthesising its own input if given none. examples/realtime_synth.py plays a two-oscillator voice and changes notes while it runs.
uv run python examples/offline_process.py out.wav
uv run python examples/realtime_synth.py 6
Real-time output
Stream plays a Graph through an audio device. The audio thread runs a Mojo callback that never enters Python, so a busy interpreter cannot make it miss a deadline: measured 0 underruns down to 16-frame blocks (0.33 ms) while Python ran flat out.
with mdsp.Stream(g) as stream: # needs PortAudio; input_device=True to capture
stream.set(filt, "cutoff", 1500.0) # queued for the audio thread
time.sleep(1.0)
print(stream.stats) # callbacks, underruns, applied, dropped, worst_render_us
While a stream runs its graph is locked: build it or change parameters through Stream.set, which never blocks and reports a full queue rather than waiting. output_devices() and input_devices() list the devices to pick from. Passing input_device makes the graph's Input nodes read live audio. PortAudio is loaded only when a stream is created, under the Linux or macOS name for the library; the rest of mdsp works without it.
Install
pip install mdsp
Wheels: Linux x86_64 and aarch64 (glibc 2.35+), macOS 13+ arm64, CPython 3.10-3.14. Free-threaded builds are not supported. Wheels bundle the Mojo runtime libraries; the Mojo compiler is not needed. Stream additionally needs PortAudio (apt install libportaudio2, brew install portaudio).
Build
Requires uv, make, and a C linker: gcc on Linux, the Xcode command line tools on macOS. The Mojo compiler is installed into .venv as a dev dependency. Targets follow Mojo's own: Linux x86_64 and aarch64 with glibc 2.35+, and macOS 13+ on Apple silicon. There is no Intel macOS or native Windows build, because Modular ships none.
make build # uv sync, then compile src/mdsp/_core.so in place
make test # Python tests, doctests, Mojo kernel tests
make qa # lint, format check, mypy, tests
make wheel # platform wheel with bundled runtime, repaired into dist/
Layout
| Path | Contents |
|---|---|
src/mdsp/_mojo/dsp/ |
Mojo kernels implementing the Processor trait |
src/mdsp/_mojo/_core.mojo |
Python bindings: Bank[P], one kernel per channel |
src/mdsp/_base.py |
Python base classes; validates buffers before they reach Mojo |
tests/mojo/ |
Kernel contract tests, run by pytest via mojo run |
scripts/hatch_build.py |
Wheel build hook: compiles and bundles the extension |
docs/dev/ |
Design spikes and decision records |
Direction
The Python block chain is the first layer. The planned next layer is a graph engine in Mojo that runs a whole graph per call, reusing the same kernels. See docs/dev/spikes/2026-09-15-dispatch-gil/.
Metadata
Release files for mdsp 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mdsp-0.1.0-py3-none-manylinux_2_35_x86_64.whl | Python 3 | none | Linux glibc 2.35+ x86-64 | Details |
| mdsp-0.1.0-py3-none-manylinux_2_35_aarch64.whl | Python 3 | none | Linux glibc 2.35+ ARM64 | Details |
| mdsp-0.1.0-py3-none-macosx_13_0_arm64.whl | Python 3 | none | macOS 13.0+ ARM64 | Details |
Total release size: 3.2 MB
Release files / mdsp-0.1.0-py3-none-manylinux_2_35_x86_64.whl
| Download URL | mdsp-0.1.0-py3-none-manylinux_2_35_x86_64.whl |
|---|---|
| Size | 1.2 MB |
| Tags | Linux glibc 2.35+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
618c28f466e9dba405e5ca3e9b97ecd8cd0d5d82f2fb4ca7e2d1deb775ffee2e
|
|
BLAKE2b-256 checksum How to use checksums |
6dbf13dba454bf88b32e55ba569d91bb4f85e7e509f07e7767bedefaf1cdcd23
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / mdsp-0.1.0-py3-none-manylinux_2_35_aarch64.whl
| Download URL | mdsp-0.1.0-py3-none-manylinux_2_35_aarch64.whl |
|---|---|
| Size | 1.1 MB |
| Tags | Linux glibc 2.35+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
f5da816c6d937a14b0f391a264d62c252c5f9a19b7dce944ede54962da2e1230
|
|
BLAKE2b-256 checksum How to use checksums |
0980cfa07dc5d145396b9fed5496a15cd3ae6fe63a19235462289e97cdb9e499
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / mdsp-0.1.0-py3-none-macosx_13_0_arm64.whl
| Download URL | mdsp-0.1.0-py3-none-macosx_13_0_arm64.whl |
|---|---|
| Size | 931.2 kB |
| Tags | Python 3 macOS 13.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
0c691a0f5e58e3ad82dd8e59b06119521444c052e4e6b53ba5b42c64069ba36e
|
|
BLAKE2b-256 checksum How to use checksums |
67619d202be00052b0032aa2d10d8e499bbda420d4b712b677098e818075b9c4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Security for mdsp 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.