utils/music_theory
Krzysztof kuhy Rudnicki af3c98a50e
Some checks failed
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
crdt_sync_dart tests / test (push) Has been cancelled
crdt_sync_flutter tests / test (push) Has been cancelled
music_theory tests / test (push) Has been cancelled
Move TODOs to todo app
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-02 11:28:31 +01:00
..
music_theory music_theory: sfx module -- noise, sweeps, biquads and 22 spellcode presets 2026-10-19 11:27:49 +02:00
.pre-commit-config.yaml deps(pypi): newest stable pins in every Python subproject 2026-10-22 21:42:26 +02:00
DOCS-article-walkthrough.md music_theory: music theory from first principles, as code you can hear 2026-09-11 18:29:13 +02:00
pyproject.toml deps(pypi): newest stable pins in every Python subproject 2026-10-22 21:42:26 +02:00
README.md music_theory: sfx module -- noise, sweeps, biquads and 22 spellcode presets 2026-10-19 11:27:49 +02:00
requirements.txt deps(pypi): newest stable pins in every Python subproject 2026-10-22 21:42:26 +02:00

music_theory

Music theory from first principles, as typed Python you can hear. Follows Luke Haas's "Music Theory for Programmers" section by section: a sound is a number changing over time, pitch is multiplicative, the tuning everyone wants cannot exist (3ⁿ ≠ 2ᵐ), so equal temperament trades purity for a system that works in every key. Scales, chords and progressions are then plain integer arrays on top of that, and a small numpy synth turns them into WAV files.

Section-by-section notes, with what to listen for, live in DOCS-article-walkthrough.md.

Install

pip install "music_theory @ git+https://github.com/kuhyx/utils@music-theory-v0.1.0#subdirectory=music_theory"

Only dependency: numpy. Playback is left to aplay (or anything that opens a 16-bit mono WAV); the package never touches an audio device itself.

Hear the article

python -m music_theory list                       # every demo and its section
python -m music_theory explain comma              # just the numbers
python -m music_theory demo all --out /tmp/mt     # WAV per section + facts
aplay -q /tmp/mt/06_comma.wav
demo article section what you hear
tone 1 sound A4, 440 Hz; the FFT peak is printed
envelope 2 envelopes same sine with a 10 ms then a 500 ms attack
waveforms 2 timbre sine, triangle, square, saw at 220 Hz
octaves 3 octaves A1 to A5, each double the last
consonance 3 consonance 2:1, 3:2, 4:3, 5:4, 16:15, then √2
comma 4 the comma A4 against A4 × 1.0136, beating 6 times a second
temperament 5 equal steps twelve steps of 2^(1/12)
divisions 5 why twelve pure fifth vs the 5-, 7-, 12-, 19-, 24-TET fifth
scale 6 scales C major, then A minor
modes 6 modes seven rotations of the same array
chords 7 qualities major, minor, dim, aug, and three sevenths on C
diatonic 8 keys I ii iii IV V vi vii° of C
transpose 8 transposition I V vi IV in C, then +4 in E
resolution 9 tension V7 then I; the tritone collapses
progressions 9 loops pop, jazz, axis, fifties
piece 11 the piece bass + arpeggio + seeded melody, A minor
bracket (beyond) the same piece clean, then degraded

Render a recipe

A composition is data. render turns a recipe JSON into a loop-length WAV and records its provenance (recipe, package version, SHA-256) in a manifest:

{"key": "A3", "mode": "major", "tempo_bpm": 112, "progression": [1, 6, 3, 7],
 "bars": 16, "seed": 20260911, "waveform": "saw", "degrade": 0.0}
python -m music_theory render --recipe city.json --out city.wav --manifest MANIFEST.json
python -m music_theory render --recipe city.json --out ruin.wav --degrade 1.0 --manifest MANIFEST.json

degrade (0..1) is the same piece, ruined: past 0.5 the mode flips to its darker relative, the tempo drags, melody notes drop out and drift off pitch, and the signal is bit-crushed, hissed and low-passed. degrade: 0 is byte- identical to the original, so "the ending is the opening, destroyed" is a one-flag transform on the same seed.

Render sound effects

sfx renders spellcode's spell and move sounds -- sfxr-style parameter sets built from this package's own primitives, no AI audio and no sfxr port -- as 16-bit mono WAVs plus a MANIFEST.json (every parameter, the seed, the package version and each file's SHA-256):

python -m music_theory sfx --out sfx/                         # all 22, 44.1 kHz
python -m music_theory sfx --out sfx/ --sample-rate 22050     # match the music beds
python -m music_theory sfx --out sfx/ --only fire_hit --only dodge

Presets: <element>_cast, <element>_hit and <element>_impact for arcane, fire, water, lightning, earth and wind, plus dodge, parry_normal, parry_perfect and fizzle. Each is seeded from its name, so a re-render is byte-identical. A sound is a few sfx.Layers (transient, body, tail, sub) mixed and normalised; define a new one with sfx_layers.make(name, *layers) and render it with sfx.render.

Modules

module article contents
pitch 1, 5 MIDI ↔ Hz (440·2^((n−69)/12)), cents, note names
tuning 3, 4, 5 harmonic series, just ratios, the comma, best_divisions
scales 6 step patterns, modes as rotations, pitch_at_degree
chords 7, 9 qualities, stacked thirds, Roman numerals, V7 → I
progressions 8, 9 named loops, degree → chord, transpose
synth 1, 2 waveforms, ADSR, low-pass, mix, normalise
sequencer 10 beat grid, NoteEvent, bar-exact render
compose 11 Recipe → bass, arpeggio, melody
degrade — the ruin transform
analysis — FFT peak, spectral centroid, band energy, octave-band slope
noise — seeded white and pink noise, Poisson grain gates
sweep — pitch curves (glide, vibrato, arpeggio, jumps, retrigger), phase-accumulating oscillator, ring mod
envelopes — exponential decay, attack-hold-punch-decay Shape
biquad — resonant low/high/band-pass with a moving cutoff
sfx, sfx_layers — sfxr-style layered parameter set and its renderer
sfx_presets*, sfx_render — spellcode's 22 presets, WAVs + manifest
wav, manifest — 16-bit mono PCM out, SHA-256 provenance

Develop

pip install -r requirements.txt
python -m pytest            # 100 % branch coverage is enforced
pre-commit run --config music_theory/.pre-commit-config.yaml --all-files   # from ~/src/utils

Consumed by ~/src/roadside-assistance (tools/render_music_beds.sh) for its three world-state music beds.