utils/music_theory/README.md
Krzysztof kuhy Rudnicki d5b3f2cff3 music_theory: music theory from first principles, as code you can hear
New subproject porting Luke Haas's "Music Theory for Programmers" section
by section into typed Python: pitch (MIDI <-> Hz), tuning (harmonic
series, the Pythagorean comma, brute-forced "why twelve"), scales as step
arrays with modes as rotations, chords stacked in thirds with Roman
numerals and the V7 -> I resolution, named progressions, a numpy synth
(waveforms, ADSR, low-pass), a bar-exact sequencer, and a seeded Recipe ->
WAV composer. `degrade` is the same piece ruined by a 0..1 amount, so an
ending can be the opening destroyed on the same seed; filtering runs LAST
because filtering first measured brighter than the clean render.

17 demos (`python -m music_theory demo all`) each print the article's
numbers -- the comma is 1.013643, the tempered fifth is 0.113 % flat,
twelve is the first acceptable division -- and the FFT of the render, so
the claims are tested rather than asserted. 157 tests, 100 % branch
coverage, ruff ALL, mypy strict, pylint 10.

Consumed next by roadside-assistance for its three world-state beds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VC17zymxkeiUcHrMvt8W5W
2026-09-11 18:29:13 +02:00

99 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# music_theory
Music theory from first principles, as typed Python you can hear. Follows
Luke Haas's ["Music Theory for Programmers"](https://runjs.app/blog/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`](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:
```json
{"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.
## 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 and spectral centroid, so claims are testable |
| `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 the utils root
```
Consumed by `~/roadside-assistance` (`tools/render_music_beds.sh`) for its
three world-state music beds.