Skip to main content

serum-render

PyPI Python versions tests license

Batch-render Serum presets to audio files. Supports Serum 1 (.fxp) and Serum 2 (.SerumPreset), on Windows and macOS, using DawDreamer as the headless engine.

One render core shared by the sequential, parallel, and CLI paths, typed jobs, stock-install plugin defaults, and a first-class answer to render reproducibility. Scope is Serum 1 + Serum 2 only, permanently.

How it works

  • Each worker process builds one DawDreamer engine per preset format, once. Jobs hot-swap presets on the matching engine (load_preset for .fxp; convert → load_state for .SerumPreset, decoded by serum2-preset-loader). The audio graph is never rebuilt mid-batch.
  • A loky pool fans jobs out across CPU cores; mixed-format batches dispatch per preset by file suffix.
  • A warmup render at engine build absorbs Serum 2's first-render lazy-load anomaly.

Requirements

  • Windows or macOS, Python 3.11–3.12
  • One or both of:
    • Serum 1 for .fxp presets — the VST2 binary. macOS: /Library/Audio/Plug-Ins/VST/Serum.vst. Windows: C:/Program Files/Common Files/VST3/Serum_x64.dll (the 64-bit VST2 really does live in the VST3 folder). The VST3 build of Serum 1 will not load .fxp correctly.
    • Serum 2 for .SerumPreset presets — the VST3. macOS: /Library/Audio/Plug-Ins/VST3/Serum2.vst3. Windows: C:/Program Files/Common Files/VST3/Serum2.vst3.
  • A valid Serum license on the machine (DawDreamer does not bypass authorization).

Install

pip install serum-render

CLI

If Serum is installed in the standard location, plugin flags are optional — serum-render finds it:

serum-render "~/Documents/Serum Presets/Leads/" ./output/

Explicit plugin paths override the defaults:

serum-render presets/ output/ \
    --serum1 "/Library/Audio/Plug-Ins/VST/Serum.vst" \
    --serum2 "/Library/Audio/Plug-Ins/VST3/Serum2.vst3"

A directory containing both .fxp and .SerumPreset files renders as one mixed batch. Common options:

Flag Default Purpose
--serum1 auto Serum 1 plugin path (VST2 binary). Needed for .fxp input.
--serum2 auto Serum 2 VST3 path. Needed for .SerumPreset input.
--note 48 MIDI note (0–127). Mutually exclusive with --midi.
--velocity 127 MIDI velocity (1–127).
--duration 1.0 Note-on duration (s).
--tail 1.0 Release silence after note-off (s).
--sample-rate 44100 Output sample rate.
--bit-depth 16 16, 24, or 32f.
--format wav wav or npy (raw float32 stereo array).
--filename-template {preset} Vars: {preset} {note} {velocity} {folder} {subpath}.
--midi Render a .mid file instead of a single note.
--workers -1 Parallel workers; -1 = cpu_count - 1.
--skip-existing off Skip presets whose output already exists.
--no-recurse off Don't descend into subdirectories.
--dry-run off Print the render plan and exit.
--json off Emit machine-readable events on stdout (see below).

Run serum-render --help for the full list.

Machine-readable output

--json writes one JSON object per line to stdout and moves every human-readable line to stderr, for driving serum-render from another program:

{"event":"start","schema":1,"total":4271,"workers":7}
{"event":"result","status":"ok","path":".../Bass 1.fxp","peak":0.3325}
{"event":"result","status":"skipped","path":"..."}
{"event":"result","status":"error","path":"...","error":"..."}
{"event":"done","ok":4268,"skipped":0,"failed":3,"elapsed":612.4}

--dry-run --json emits the start event alone and exits. Exit codes are unchanged: 0 clean, 1 some renders failed, 2 usage or validation error. Validation errors write to stderr and produce no events, so exit 0 always means at least one done event — a run that found no presets still emits start with total: 0 and a zeroed done.

Two rules for consumers. Reject an unrecognised schema rather than guessing at the shape. And skip any stdout line that fails to parse: loky workers inherit the parent's stdout, so a plugin printing from C could in principle interleave with the stream — trust the counts in done over a local tally of result events.

The output shape is unstable until 1.0.

Library API

from serum_render import RenderConfig, Renderer, ParallelRenderer, render_preset

config = RenderConfig(
    serum1_plugin_path="/Library/Audio/Plug-Ins/VST/Serum.vst",
    serum2_plugin_path="/Library/Audio/Plug-Ins/VST3/Serum2.vst3",
    note=48,
    duration=1.0,
    tail=1.0,
)

# Sequential — one engine per format, reused across renders
with Renderer(config) as r:
    audio = r.render("lead.fxp")          # auto-detected as Serum 1
    audio = r.render("pad.SerumPreset")   # auto-detected as Serum 2

# Parallel mixed batch — dict of path -> (2, N) float32 array
with ParallelRenderer(config, workers=-1) as r:
    results = r.render_batch(["a.fxp", "b.fxp", "c.SerumPreset"])

# One-off
audio = render_preset("lead.fxp", config)

Set only the plugin paths for the formats you render; a missing path for a format actually present in the batch raises ValueError naming the field, before any worker boots.

Reproducibility

By default, batch renders are not bit-reproducible: Serum keeps internal DSP state across consecutive renders (LFO phase, envelope residue, lazy-loaded sample data), so a preset rendered mid-batch differs from the same preset rendered alone — measured at 97% of factory presets.

--deterministic (or RenderConfig(deterministic=True)) fixes this: every preset renders in a fresh single-use process, fanned out across your worker count, making batch output bit-identical across runs and render orders. The cost is one plugin load per preset instead of per worker — use it when reproducibility matters (ML datasets, regression baselines), skip it when you just want samples fast.

serum-render presets/ output/ --deterministic

Why a whole process per preset? Serum 2 can be reset by reloading the plugin in place, but Serum 1 keeps state in library-level globals that survive even a full engine rebuild — only process isolation resets both. Probe data and methodology: docs/decisions.md, raw numbers in docs/determinism-probe-2026-07-16.json. Full caveat list: KNOWN_ISSUES.md.

Development

python3.12 -m venv .venv
.venv/bin/pip install -e ".[dev]"

# fast unit tests (no plugin required)
.venv/bin/pytest tests/ --ignore=tests/test_serum1_smoke.py --ignore=tests/test_serum2_smoke.py

# integration smokes (real Serum installs; each half gated independently)
.venv/bin/pytest tests/test_serum1_smoke.py tests/test_serum2_smoke.py \
    --serum1-plugin-path "/Library/Audio/Plug-Ins/VST/Serum.vst" \
    --serum2-plugin-path "/Library/Audio/Plug-Ins/VST3/Serum2.vst3" \
    --serum1-preset-dir  "$HOME/Documents/Serum Presets/Leads/" \
    --serum2-preset-dir  "$HOME/Documents/Serum 2 Presets/Pads/"

Env vars SERUM1_PLUGIN_PATH, SERUM2_PLUGIN_PATH, SERUM1_PRESET_DIR, SERUM2_PRESET_DIR work as flag alternatives.

License

GPL-3.0 (inherited from DawDreamer).

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

serum_render-0.3.0.tar.gz (74.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

serum_render-0.3.0-py3-none-any.whl (41.7 kB view details)

Uploaded Python 3

File details

Details for the file serum_render-0.3.0.tar.gz.

File metadata

  • Download URL: serum_render-0.3.0.tar.gz
  • Upload date:
  • Size: 74.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for serum_render-0.3.0.tar.gz
Algorithm Hash digest
SHA256 cea09badb31ad2d5caed155e50281fb9364e5b7d47e25a283544fa9858be086c
MD5 99491f1b49bc140701a148bdc4d9102e
BLAKE2b-256 a2c1f64cd6f1fc5e01621107b341e07eb5286b3464e8ed44864424922b039496

See more details on using hashes here.

Provenance

The following attestation bundles were made for serum_render-0.3.0.tar.gz:

Publisher: publish.yml on wiillownet/serum-render

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file serum_render-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: serum_render-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 41.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for serum_render-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9e078ed00a1f83b6e4458dfdc6192e408af8fd101aff03bc41a9277d4d6cee13
MD5 87faa5662e1cb74c2e3380d691c60de4
BLAKE2b-256 09311075f8c2139721c66ffe0aec06a2f884565cd2e41c41a74749533777ead0

See more details on using hashes here.

Provenance

The following attestation bundles were made for serum_render-0.3.0-py3-none-any.whl:

Publisher: publish.yml on wiillownet/serum-render

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.5.0

2 files

0.4.0

2 files

0.3.1

2 files

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.0

2 files

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