|
|
||
|---|---|---|
| .. | ||
| examples | ||
| music_theory | ||
| .pre-commit-config.yaml | ||
| DOCS-article-walkthrough.md | ||
| pyproject.toml | ||
| README.md | ||
| requirements.txt | ||
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 a score
python -m music_theory render --score examples/waltz.txt --out waltz.wav
Named voices, noise drums, meter, A/B sections. Format: score.py
docstring; examples: examples/waltz.txt (3/4), examples/jig.txt (6/8).
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 --cov=music_theory # 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.