utils/music_theory/README.md

134 lines
5.9 KiB
Markdown
Raw Permalink 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.
## 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.Layer`s
(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.