Greek κανών: the measuring rod, and the name of the Pythagorean monochord — the one-stringed instrument used to measure musical intervals.
A desktop app that measures an album's tracks against your own reference tracks and writes self-contained HTML reports. Runs on macOS and Windows.
Measurements only. The reports state what was measured and how it differs from your references. They never interpret a number, rank it, or suggest a move — that is the engineer's job. Every metric carries a one-line plain-English note explaining what it measures and why anyone looks at it.
One ruler. Every figure comes from this tool at the version stamped in the report footer. Readings from other analysers are measured differently and will not line up; comparing across tools is how you end up chasing a number that was never the same number.
Every push builds KANON for Windows and macOS on GitHub Actions — neither platform can cross-compile the other's bundle, so each is built on its own runner. Grab the one for your machine from the latest release, or from the Artifacts section of the most recent run under the Actions tab if no release is tagged yet.
| Download | For |
|---|---|
KANON-windows-x64.zip |
Windows 10/11. Unzip and keep the whole folder together; run KANON.exe inside it. |
KANON-macos-apple-silicon.zip |
Macs with M1/M2/M3/M4. |
KANON-macos-intel.zip |
Older Intel Macs. |
Not sure which Mac? Apple menu → About This Mac. "Apple M-something" is Apple silicon; anything saying Intel takes the Intel download.
First launch on macOS. These builds are unsigned, so Gatekeeper refuses a plain double-click. Right-click the app → Open → Open — once only; after that it launches normally. If macOS still blocks it:
xattr -cr /path/to/KANON.appKANON needs Tk, and how you get it depends on which Python you have.
# If you use Homebrew Python, Tk is a separate package:
brew install python-tk
# The python.org installer bundles Tcl/Tk already — nothing extra needed.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python -m app.mainCheck Tk is present before anything else — python3 -m tkinter should open a
small test window. If it raises ModuleNotFoundError: No module named '_tkinter', that is the missing python-tk above.
MP3 reading comes from libsndfile, which ships inside the soundfile wheel;
there is nothing to install with Homebrew for audio.
py -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
python -m app.mainpython -m app.mainTwo panes: the album queue (ordered — the summary reports the change at each gap) and references (measured with the same ruler, never part of the sequence). Add files or a whole folder, reorder with the arrows, press Run analysis. Analysis runs on a worker thread, so the window stays live; each row shows its own status and grows a Report button when its page is ready.
Reports are written next to the source files unless you pick an output folder:
<track>__report.html— one per track<folder>__album.html— the album summary
python cli.py "album/*.wav" --ref "refs/*.wav" --album --out-dir reportspython -m core.analyze "track.wav"Loudness — integrated LUFS, max short-term (3 s), max momentary (400 ms), LRA, PLR, PSR, and a macro-dynamics curve of short-term loudness across the track.
Peaks and integrity — sample peak and 4x-oversampled true peak per channel, full-scale runs, DC offset, noise floor (the quietest 5 % of 400 ms windows) with its spectral shape, and mains-hum prominence at the 50 Hz and 60 Hz families measured on the quiet windows.
Spectral — seven mastering bands (sub, low, low-mid "mud", mid, presence "harshness", high, air), each with energy relative to total, crest factor, envelope depth, presence fraction, stereo width and L/R correlation; plus a 1/6-octave LTAS overlaid on the references, spectral tilt in dB/octave, and a sibilance reading for 5–10 kHz.
Stereo — full-band and sub-150 Hz correlation, width by band, and the mono-fold delta: the loudness that actually dies when the record is summed.
Album — every track against every headline metric, the change at each gap in the sequence, and each track's distance from the album median.
Both exist because a percentile over a whole file otherwise measures the arrangement rather than the sound. Both are stated in the reports.
- Activity gate. Envelope statistics only count frames within 40 dB of the track's own p95 level, so leading silence and fade tails do not become "dynamics".
- Band presence gate. A band more than 40 dB below its own loud level counts as absent, not quiet — otherwise a breakdown with no bass in it turns into 60 dB of "sub dynamics". The share of time a band is present is reported separately, so absence is visible instead of hidden inside depth.
Band envelopes also use a window of roughly two cycles of the band's bottom edge (100 ms at the sub band, 10 ms up top). A 5 ms window is shorter than one cycle of a 25 Hz wave and would measure the waveform rather than its level.
core/ pure measurement: audio in, JSON-able dict out. No UI imports.
audio.py loading, folding, filters, envelopes, activity gate
loudness.py LUFS / LRA / PLR / PSR / macro curve (K-weighting + gating)
peaks.py true peak, clipping, DC, noise floor, hum
spectral.py bands, LTAS, tilt, width, correlation, sibilance
analyze.py orchestrator + `python -m core.analyze`
report/ metrics dicts in, self-contained HTML out
teaching.py the one-line note for every metric
plots.py matplotlib figures as base64 PNG data URIs
render.py per-track page
album.py album summary page
app/ customtkinter shell (worker thread, queue + references panes)
build/ build.ps1: PyInstaller + smoke test
tests/ selftest.py, test_reports.py, test_gui_smoke.py
The measurement core is ported from the recovery-analysis instrument in the
lets-make-dnb studio — band-split envelopes, percentile depth, mono
correlation, Welch band energies — with all the genre logic (tempo, onset
epoching, duck/bloom, drop windows) removed. That project is a separate silo
and is never written to.
numpy, scipy, soundfile, pyloudnorm, matplotlib, customtkinter. Verified on Python 3.14.
librosa is deliberately not a dependency — it drags in numba and llvmlite and would more than double the build. The build script fails if anything imports it.
matplotlib is pinned to the Agg backend and driven through the object API rather than pyplot, because figures are rendered on the worker thread and pyplot's global figure registry is not thread-safe.
The Mac delivery is run-from-source (above); this is for the Windows box.
.\build\build.ps1Produces build\dist\KANON\ (--onedir; onefile would unpack a 160 MB
scientific stack to a temp folder on every launch). The script then runs the
built exe with --selftest, which measures a generated file and writes a
report — a windowed build has no console, so that file is how a missing DLL
announces itself instead of a window that never appears.
Ship the whole KANON folder, not just the .exe.
build.ps1 is Windows-only (PowerShell, .exe paths). A macOS .app bundle
would need its own PyInstaller invocation — that is the planned GitHub Actions
matrix build, not something this script does.
python tests/selftest.py "some track.wav" # measurement ruler
python tests/test_reports.py # HTML structure + no-recommendations rule
python tests/test_gui_smoke.py # drives the real window end to endselftest.py checks synthetic signals with known answers and cross-checks our
own BS.1770 gating against pyloudnorm. test_reports.py scans the rendered
pages for advice-shaped language and fails on it — the no-recommendations rule
is enforced, not just intended.