Skip to main content

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)

Table of built distributions (wheels) for mdsp 0.1.0
File Interpreter ABI Platform
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

3 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page