English · Español
DAW user? Get the VST3 plugin (Windows).
Unsigned beta: Windows may warn — click More info → Run anyway. macOS & other downloads on the releases page.
A deterministic automix + automaster engine. Give it a folder of finished, FX-printed stems; it loudness-balances them into a stereo mix and masters that mix to a clean, streaming-safe loudness — built on ITU-R BS.1770-4 metering and a true-peak-limited master chain. Same stems in, same master out, every run. No AI guessing, no randomness.
What it does, in one line: turn a pile of finished stems into a balanced mix and a competitive, peak-safe master with one command — without touching your tone.
Keel is for two people. The producer with great stems who doesn't want to (or
can't) mix and master by hand: one command, done. The engineer who wants a
deterministic, scriptable balance+master stage in a pipeline: exact LUFS targets,
a real oversampled true-peak meter, reproducible to the sample, with a QC report
on every run. The long-term goal is a standalone GUI and a VST/plugin on
the same engine — see ROADMAP.md.
Keywords: automatic mixing, automatic mastering, loudness normalization, LUFS, true peak, ITU-R BS.1770-4, stem balancing, limiter, mastering chain, deterministic audio, reproducible, Python audio, VST (planned).
| Input | Any number of finished, FX-printed stems (.wav / .flac) |
| Output | Balanced stereo mix + loudness-safe master (24-bit WAV) + REPORT.md |
| Loudness | Normalized to an exact LUFS target (default -14), ITU-R BS.1770-4 |
| Peaks | Real 4x-oversampled true-peak limiting to a dBTP ceiling (default -1.0) |
| Labeling | Auto-detected, user-editable keel.json; 1..N files per label |
| Mastering | Internal clip -> limit chain, or match a commercial reference (Matchering) |
| Determinism | Same inputs -> identical output. No ML, no randomness in the render path |
| Tone | Untouched — Keel balances and masters; it never re-EQs your stems |
A real 8-stem multitrack (Tally Hall, "Good Day", 3.5 min) — auto-labeled and mastered with defaults, no manual editing:
$ python build.py --stems ./GoodDay --out out
mapping -> GoodDay/keel.json (auto-detected: guitar, vocals, bass, drums, synth)
mix -> ..._mix.wav 210.0s -6.0 dBFS groups: guitarx3, vocalsx2, bass, drums, synth
master -> ..._master.wav -14.0 LUFS -2.76 dBTP
From the generated out/REPORT.md:
| label | files | pre LUFS | gain dB | post LUFS |
|---|---|---|---|---|
| guitar | 3 | -26.81 | +3.31 | -23.5 |
| vocals | 2 | -24.51 | +4.51 | -20.0 |
| bass | 1 | -28.87 | +5.87 | -23.0 |
| drums | 1 | -28.50 | +6.50 | -22.0 |
| synth | 1 | -27.93 | +1.93 | -26.0 |
Master: -14.0 LUFS (exactly on target), -2.76 dBTP (safely under the -1.0 ceiling). Three guitars and two vocals were each balanced as a single group. Re-running reproduces these numbers to the sample.
| Stage | Does | Does NOT |
|---|---|---|
| Mix | Groups files by label, loudness-balances each group to a target, pans only if asked, sums to stereo, leaves headroom for the master. | Add EQ, compression, or reverb. Your stems are already treated — Keel will not touch your tone. |
| Master | Brings the mix to an exact LUFS target; controls peaks with an oversampled soft-clip + true-peak limiter at a dBTP ceiling; optionally matches a reference track. | Re-balance instruments. A stereo master can't fix a mix — fix the balance in the mix stage and re-run. |
MIX stems -> [group by label] -> [loudness-balance each group] -> [pan?] -> sum -> mix.wav
MASTER mix -> [tone/glue] -> [pre-normalize] -> [oversampled soft-clip]
-> [4x true-peak limiter] -> [normalize to exact LUFS] -> [TP safety] -> master.wav
Every step is measurement-driven and deterministic. The mix leaves the bus near -6 dBFS so the master has room; the master normalizes to your exact LUFS target and guarantees the true-peak ceiling.
Recommended — a local virtual environment (keeps Keel's deps isolated, runs the
same on any machine). setup.ps1 builds .venv and installs the core engine
offline from the vendored wheels:
.\setup.ps1 # create .venv + install core engine offline from vendor/
.\setup.ps1 -Online # ...or pull from PyPI instead
.\setup.ps1 -Matchering # also install the optional reference-master path
.\.venv\Scripts\Activate.ps1 # activate, then run build.pyThe venv itself is never committed — it is rebuilt from requirements.txt /
vendor/ on each machine.
Prefer to install straight into your current Python?
pip install -r requirements.txt # numpy, scipy, soundfile, pyloudnorm, pedalboardOffline? The wheels are vendored:
python -m pip install --no-index --find-links vendor numpy scipy soundfile pyloudnorm pedalboardOptional: matchering for the reference-matched master path (vendored too, but it
pulls a heavy numba/llvmlite/pandas tree — .\setup.ps1 -Matchering or
pip install matchering).
1. Put your stems in a folder — any number, any names. Keel does not require a fixed set of stems; it auto-detects a label per file, and you correct it next.
my_song/
drum_kick.wav drum_snare.wav oh_L.wav oh_R.wav
bass.wav
gtr_DI_1.wav gtr_DI_2.wav gtr_solo.wav
lead_vox.wav harmony.wav
2. Run it:
python build.py --stems "C:\path\to\my_song" --out outOn the first run Keel writes my_song/keel.json (auto-detected file -> label
map + per-label balance) and renders out/my_song_mix.wav,
out/my_song_master.wav, and out/REPORT.md.
3. Fix the labels, then re-run. Auto-detect is only a guess. Open keel.json,
reassign any file to the right label (a label can hold 1 or 10 files — they're
balanced as one group), tweak the per-label balance, and run the same command
again:
{
"stems": { "oh_L.wav": "drums", "gtr_solo.wav": "guitar", "harmony.wav": "vocals" },
"balance": { "vocals": 0.0, "drums": -2.0, "guitar": -3.5 }
}python build.py --stems ./my_song --scan # only (re)write keel.json, no render
python build.py --stems ./my_song --map "C:\maps\x.json" # use a mapping file elsewhere
python build.py --stems ./my_song --preset loud # house-sound loudness profile
python build.py --list-presets # list the named presets
python build.py --stems ./my_song --lufs -11 --tp -1 # push louder, set TP ceiling
python build.py --stems ./my_song --ref "C:\refs\ref.wav" # match a reference (Matchering)
python build.py --stems ./my_song --mix-only # stop after the mix
python build.py --stems ./my_song --master-only # remaster an existing mix
python build.py --batch "C:\path\to\album" --out out # every subfolder (its own keel.json)There is no fixed stem list. Each file gets a label; all files sharing a label
are balanced together as one group (so two guitars hit the target together instead
of coming out twice as loud). Auto-detect seeds the map from filenames; you own it
from there. Custom labels and other are fine and default to 0.0 balance.
{
"stems": { "kick.wav": "drums", "gtr_DI_1.wav": "guitar", "lead_vox.wav": "vocals" },
"balance": { "vocals": 0.0, "drums": -2.0, "bass": -3.0, "guitar": -3.5, "synth": -6.0 },
"pan": {},
"spread": {},
"glue": false,
"master": { "target_lufs": -14.0, "tp_ceiling_db": -1.0, "reference": null }
}- balance — relative loudness in LU vs. the vocal (vocals = 0). More negative
= quieter. Seeded from
recipes.py(DEFAULT_BALANCE). - pan —
-1.0(L) ..+1.0(R), equal-power. Default centered (your printed width is kept). - spread —
0..1; auto-pans a multi-file label symmetrically (e.g. doubled guitars hard L/R). Default 0. - glue —
true/false; a gentle bus-glue compressor over the summed mix. Defaultfalse(the stems are already mix-ready). CLI--glue/--no-glueoverrides it. Leave off unless a sum genuinely wants light cohesion. - master —
target_lufs,tp_ceiling_db, and an optionalreferencefilename. CLI--lufs/--tp/--refoverride these.
Instead of remembering numbers, pick a named target with --preset. A preset
sets only the master loudness target + true-peak ceiling (it picks how loud the
master lands, not how the instruments sit), applied at render time over the
mapping's master block. An explicit --lufs/--tp still wins over a preset.
| preset | target | ceiling | for |
|---|---|---|---|
streaming (default) |
-14 LUFS | -1 dBTP | Spotify / YouTube / Tidal / Amazon normalization target |
loud |
-10 LUFS | -1 dBTP | club / aggressive; held clean by the oversampled clip + true-peak limiter |
broadcast |
-16 LUFS | -1 dBTP | Apple Music / Apple Podcasts / AES TD1008 — quieter, more dynamic |
python build.py --stems ./my_song --preset loud
python build.py --list-presets- Loudness: integrated LUFS via
pyloudnorm(ITU-R BS.1770-4, 400 ms gated). - True peak: a real 4x polyphase-FIR oversampling meter (scipy
resample_poly, Kaiser beta 12) — catches intersample peaks a sample-peak meter misses (up to ~+3 dB). - Master chain: tone (HPF 28 / low-shelf / air / gentle glue) -> pre-normalize -> oversampled tanh soft-clip (rounds the sharpest transients) -> 4x-oversampled true-peak limiter -> normalize to the exact target -> true-peak safety. The clip-then-limit pairing is the loud-but-clean approach: the clipper takes the very top so the limiter stays clean.
- Reference master (optional):
matcheringmatches a commercial reference's spectrum, loudness, and stereo width; the reference then sets the loudness. - Defaults: master -14.0 LUFS / -1.0 dBTP (streaming-optimal); per-stem internal anchor -20 LUFS. The chain pushes to -10/-11 cleanly when asked.
Every build.py run writes out/REPORT.md: per-label balance (pre/post LUFS +
gain) and the final master LUFS/dBTP vs. target — one glance to confirm it landed.
keel.py # public library API: one import surface for CLI/GUI/plugin
gui.py # desktop GUI (PySide6): drag-folder -> faders/meters -> one-click render
userpresets.py # user-saved loudness presets (small JSON store; GUI save/load)
build.py # CLI entry point: scan -> write/read keel.json -> mix -> master -> REPORT
tests/ # stdlib-unittest safety net (determinism, landing, balance, labeling)
recipes.py # default balance/pan/master tables + the auto-detect alias hints
mixer.py # the mix engine: autodetect/group by label, loudness-balance, pan, sum
mastering.py # the master engine: tone -> clip -> true-peak limit -> exact LUFS, or Matchering
meters.py # LUFS (BS.1770-4) + 4x true-peak meter; shared gain math
<song>/keel.json# per-song mapping: file -> label, per-label balance/pan/spread, master target
out/ # rendered mixes, masters, and REPORT.md (build artifacts)
vendor/ # offline pip wheels
The engine is deterministic and self-contained: no network, no telemetry, no project lock-in. Outputs are plain 24-bit WAVs.
Every front-end drives the same core through keel.py — the DSP is never forked
per front-end:
import keel
mapping = keel.autodetect("my_song") # {file: label}
recipe = keel.mix_recipe({"balance": {"synth": -4.0}}) # defaults + overrides
keel.mix("my_song", recipe, "out/song_mix.wav", mapping=mapping)
keel.master("out/song_mix.wav",
keel.master_recipe(keel.preset_master("streaming")),
"out/song_master.wav")A stdlib-unittest suite synthesizes tiny stems and checks the guarantees
(deterministic byte-identical output, exact master LUFS under the true-peak
ceiling, group balance, label matching) — no committed audio, no extra deps:
.\.venv\Scripts\python.exe -m unittest discover -s testsA standalone window drives the same engine — drop a folder of stems, tweak the balance, click once for mix + master. Built on PySide6 (Qt), installed online (the Qt binaries are large, so they are not vendored offline like the core engine):
.\setup.ps1 -Gui # adds PySide6 to the venv (or: pip install -r requirements-gui.txt)
python gui.pyWhat the scaffold does today: drag-a-folder / open-folder with auto-detected
labels in an editable table, per-label balance faders (relative LU), a
loudness preset picker with save/load of your own presets, an optional
reference picker, a bus-glue toggle, one-click render to mix + master in
a background thread, post-render LUFS / true-peak meters (the same
meters.py math the engine uses), and save/load of the whole project
(keel.json). Real-time playback metering and a signed installer are next (see
ROADMAP.md Phase 4).
GitHub Actions builds standalone executables on every version tag (and on demand from the Actions -> build-app -> Run workflow button):
- Windows:
Keel.exe— a single onefile, just run it. - macOS (Apple Silicon):
Keel.dmg— open it and drag Keel to Applications. It is currently unsigned, so first launch needs right-click -> Open. (Intel Macs aren't built yet — ask and amacos-13job can be added.)
Grab them from the latest build-app run's artifacts. Each build self-tests the frozen app before it's uploaded.
.\setup.ps1 -Gui
.\.venv\Scripts\python.exe -m pip install -r requirements-build.txt
.\.venv\Scripts\pyinstaller Keel.spec --noconfirm # -> dist\Keel.exe (or dist/Keel.app on macOS)Today Keel is a command-line tool. The mission is to reach any musician on the same deterministic engine:
- Standalone GUI — drag a folder of stems, see the labels and loudness meters, get your mix + master.
- VST / plugin — run Keel's balance + master stage inside your DAW.
The DSP core is done and validated. See ROADMAP.md for the plan
and docs/adr/ for the decision records (why the engine, config,
toolkit, licensing, and packaging are the way they are).
Keel is free for the people it is built for, and stays alive on donations. If it saved you a mix-and-master fee or a monthly subscription, you can chip in:
- PayPal: Donate
(or send to
fcarvajalbrown@protonmail.com) - Or use the Sponsor button at the top of the GitHub repo.
Donations are voluntary and fund development; they are not a purchase and grant no commercial rights.
Keel is licensed in two parts.
- The engine (Python library + CLI) is GNU AGPL-3.0 (
LICENSE) — free and open source. Copyleft: distribute it or run a modified version as a network service and you must release your source under the AGPL too. - The desktop GUI (the
Keel.exe/Keel.appapp) is free for non-commercial use and free for individual musicians making their own music — even if you sell it — under the PolyForm Noncommercial License plus an additional grant (LICENSE-NONCOMMERCIAL.md).
We do not charge for the app. A commercial license
(COMMERCIAL-LICENSE.md) — USD 20, one-time, per
seat — is a commercial-use license, required only when Keel is used to earn
from other people's material or to run a business on it: record labels,
professional mixing / mastering studios and freelance engineers doing paid
client work, offering Keel as a paid product/service, redistributing it inside
another product, or building the engine into a closed-source product without the
AGPL's copyleft. Being a pro doesn't trigger the fee — paid client work or running
a label/studio/business does.
In one line: make your own music with it for free, even as a pro selling your own records; pay only if you run a business on it or charge clients with it.
Felipe Carvajal Brown — fcarvajalbrown@gmail.com
Copyright (C) 2026 Felipe Carvajal Brown.
