mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 960fdbe3df | |||
| b3f3e985b4 | |||
| c1925af69d | |||
| 7883c978f7 | |||
| 36d558573c | |||
| 1e2f09e2ab | |||
| 9404afc1f3 | |||
| 72aa097552 | |||
| 5ebf0bdd97 | |||
| 1d897c6609 | |||
| 4113aad5d0 | |||
| 6ecef688e1 | |||
| fcc5db8e3d | |||
| 9de113b6e7 | |||
| 0b98f7bd77 | |||
| e0a1ce9d18 | |||
| de7575fe0a | |||
| 665a6f5de5 | |||
| 63362df697 | |||
| 755b33a63b |
@@ -7,3 +7,4 @@ t2.py
|
||||
__pycache__
|
||||
pytheory.egg-info
|
||||
docs/_build
|
||||
.claude/worktrees/
|
||||
|
||||
+104
@@ -2,6 +2,110 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.42.1
|
||||
|
||||
- **Fretboard tuning support** — `to_tab()` now accepts `Fretboard` objects as
|
||||
the `tuning` parameter. Works with `Fretboard.guitar()`, `Fretboard.bass()`,
|
||||
`Fretboard.ukulele()`, `Fretboard.mandolin()`, `Fretboard.banjo()`, and any
|
||||
custom Fretboard with capo.
|
||||
|
||||
## 0.42.0
|
||||
|
||||
- **LilyPond export** — `Score.to_lilypond()` generates complete LilyPond source
|
||||
files with multi-staff scores, key/time signatures, tempo markings, and
|
||||
automatic bass clef detection. Output can be compiled to publication-quality
|
||||
PDFs with LilyPond.
|
||||
- **MusicXML export** — `Score.to_musicxml()` generates MusicXML 4.0 documents
|
||||
that can be opened in MuseScore, Sibelius, Finale, and any notation software.
|
||||
Includes proper ties, chords, clef detection, and tempo/time signature metadata.
|
||||
- **Guitar/bass tablature** — `Part.to_tab()` and `Score.to_tab()` generate ASCII
|
||||
tablature. Supports guitar (6-string), bass (4-string), drop D, and custom
|
||||
tunings. Automatically maps notes to the best string/fret positions.
|
||||
|
||||
## 0.41.4
|
||||
|
||||
- **Fix** — `to_abc()` now ties long notes across barlines instead of emitting
|
||||
oversized durations that abcjs can't render (e.g. 16-beat notes become four
|
||||
tied whole notes).
|
||||
|
||||
## 0.41.3
|
||||
|
||||
- **Fix** — `to_abc()` now skips parts with only drum tones or rests (no pitched
|
||||
notes), fixing "pitch is undefined" errors in abcjs. Chords are correctly
|
||||
recognized as pitched content.
|
||||
|
||||
## 0.41.2
|
||||
|
||||
- **Auto bass clef** — `to_abc()` detects low-register parts (808, bass, timpani)
|
||||
and assigns `clef=bass` automatically based on average note octave.
|
||||
|
||||
## 0.41.1
|
||||
|
||||
- **Fix** — `to_abc()` no longer crashes on parts containing drum tones.
|
||||
|
||||
## 0.41.0
|
||||
|
||||
- **ABC notation export** — `Score.to_abc()` converts scores to ABC notation
|
||||
strings. Supports multi-voice scores (via `V:` directives), chords, rests,
|
||||
accidentals, and all standard durations. Pass `html=True` to get a
|
||||
self-contained HTML page that renders sheet music in the browser via abcjs.
|
||||
|
||||
## 0.40.9
|
||||
|
||||
- **Mellotron synth** — tape-replay keyboard with wow/flutter, tape saturation,
|
||||
bandwidth limiting, hiss, and 8-second tape fadeout. Three tape banks via the
|
||||
`tape` parameter: `"strings"` (default), `"flute"`, and `"choir"`.
|
||||
- **Analog oscillator synths** — four new waveform generators for fat, alive,
|
||||
analog-style sounds:
|
||||
- `Synth.HARD_SYNC` — slave oscillator hard-synced to a master (Prophet-5
|
||||
leads). `slave_ratio` parameter controls harmonic content.
|
||||
- `Synth.RING_MOD` — two oscillators multiplied for metallic, bell-like
|
||||
inharmonic tones. `mod_ratio` parameter.
|
||||
- `Synth.WAVEFOLD` — west coast wavefolding (Buchla-style). `folds` parameter
|
||||
sweeps from warm to gnarly.
|
||||
- `Synth.DRIFT` — analog VCO with pitch drift, jitter, and noise floor.
|
||||
`shape` parameter (`"saw"`, `"square"`, `"triangle"`, `"pulse"`) and
|
||||
`drift_amount` for instability level.
|
||||
- **Synth kwargs passthrough** — `play()`, `save()`, and `_render()` now accept
|
||||
`**synth_kw` for forwarding parameters to synth wave functions (e.g.
|
||||
`play(tone, synth=Synth.MELLOTRON, tape="choir")`).
|
||||
- **14 new instrument presets** — `mellotron`, `mellotron_strings`,
|
||||
`mellotron_flute`, `mellotron_choir`, `sync_lead`, `sync_lead_bright`,
|
||||
`ring_mod_bell`, `ring_mod_metallic`, `wavefold_warm`, `wavefold_gnarly`,
|
||||
`drift_saw`, `drift_square`, `analog_pad`, `analog_bass`.
|
||||
- **808 bass envelope fix** — changed from `pluck` (zero sustain, wrong for 808)
|
||||
to `piano` (sharp attack with long decay tail).
|
||||
|
||||
## 0.40.8
|
||||
|
||||
- **Fix hold() inflating duration** — `Note.beats` was returning the full
|
||||
duration for held notes (`_hold=True`), causing `Part.total_beats` and
|
||||
`Score.duration_ms` to overcount. A part with `hold(Sa, WHOLE * 4)` followed
|
||||
by `add(Pa, QUARTER)` would report 17 beats instead of 1. Now held notes
|
||||
return 0 beats, matching the renderer which already skipped advancing the
|
||||
timeline for held notes.
|
||||
|
||||
## 0.40.7
|
||||
|
||||
- **Expose missing Synth enum entries** — rhodes, wurlitzer, vibraphone,
|
||||
pipe organ, and choir wave functions were already implemented but not
|
||||
accessible via the Synth enum. Now available as `Synth.RHODES`,
|
||||
`Synth.WURLITZER`, `Synth.VIBRAPHONE`, `Synth.PIPE_ORGAN`, `Synth.CHOIR`.
|
||||
|
||||
## 0.40.6
|
||||
|
||||
- **Saxophone presets cleaned up** — removed lowpass filters and vel_to_filter
|
||||
from all sax instrument presets (saxophone, alto_sax, tenor_sax, bari_sax).
|
||||
The saxophone wave function already shapes its own spectrum; the extra
|
||||
filters were dulling the tone.
|
||||
|
||||
## 0.40.5
|
||||
|
||||
- **Saxophone synth overhaul** — reed nonlinearity (asymmetric soft clipping),
|
||||
conical bore formant resonances, breath noise with attack envelope, separate
|
||||
reed buzz, key click transient, and sub-harmonic warmth. Vibrato dialed back
|
||||
to subtle, delayed onset.
|
||||
|
||||
## 0.40.4
|
||||
|
||||
- **Distortion overhaul** — multi-stage clipping (preamp → power amp →
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
"""Demo the 5 new synths: Mellotron, Hard Sync, Ring Mod, Wavefold, Drift.
|
||||
|
||||
Each synth gets a short musical phrase — not just a scale run — with
|
||||
reverb and rhythmic variety to show off its character.
|
||||
"""
|
||||
from pytheory import Score, Duration, play_score
|
||||
|
||||
EIGHTH = Duration.EIGHTH
|
||||
QUARTER = Duration.QUARTER
|
||||
HALF = Duration.HALF
|
||||
DOTTED_Q = Duration.DOTTED_QUARTER
|
||||
WHOLE = Duration.WHOLE
|
||||
|
||||
|
||||
# ── Mellotron Strings ────────────────────────────────────────────────────────
|
||||
# Strawberry Fields vibes — slow, haunted, with rests that breathe.
|
||||
|
||||
print("=== MELLOTRON STRINGS ===")
|
||||
s = Score("4/4", bpm=72)
|
||||
p = s.part("tape", instrument="mellotron_strings",
|
||||
reverb=0.45, reverb_type="cathedral", reverb_decay=2.0)
|
||||
p.add("G4", HALF).add("B4", QUARTER).add("D5", QUARTER)
|
||||
p.add("C5", DOTTED_Q).add("B4", EIGHTH).add("A4", HALF)
|
||||
p.rest(QUARTER)
|
||||
p.add("G4", DOTTED_Q).add("F#4", EIGHTH).add("G4", WHOLE)
|
||||
play_score(s)
|
||||
|
||||
|
||||
# ── Mellotron Flute ──────────────────────────────────────────────────────────
|
||||
# Lonely, breathy, with space between phrases.
|
||||
|
||||
print("\n=== MELLOTRON FLUTE ===")
|
||||
s = Score("3/4", bpm=84)
|
||||
p = s.part("flute", instrument="mellotron_flute",
|
||||
reverb=0.5, reverb_type="taj_mahal", reverb_decay=2.5)
|
||||
p.add("E5", HALF).add("D5", QUARTER)
|
||||
p.add("C5", DOTTED_Q).add("B4", EIGHTH).rest(QUARTER)
|
||||
p.add("A4", HALF).add("G4", QUARTER)
|
||||
p.add("A4", HALF).rest(QUARTER)
|
||||
p.add("E5", QUARTER).add("D5", QUARTER).add("C5", QUARTER)
|
||||
p.add("B4", HALF + QUARTER)
|
||||
play_score(s)
|
||||
|
||||
|
||||
# ── Mellotron Choir ──────────────────────────────────────────────────────────
|
||||
# Ghostly pad — slow chords, big reverb.
|
||||
|
||||
print("\n=== MELLOTRON CHOIR ===")
|
||||
s = Score("4/4", bpm=60)
|
||||
p = s.part("choir", instrument="mellotron_choir",
|
||||
reverb=0.6, reverb_type="cathedral", reverb_decay=3.0)
|
||||
p.add("C4", WHOLE)
|
||||
p.add("E4", HALF).add("G4", HALF)
|
||||
p.add("A4", DOTTED_Q).add("G4", EIGHTH).add("F4", HALF)
|
||||
p.add("E4", WHOLE)
|
||||
play_score(s)
|
||||
|
||||
|
||||
# ── Hard Sync Lead ───────────────────────────────────────────────────────────
|
||||
# Aggressive, punchy — fast 16ths and syncopation.
|
||||
|
||||
print("\n=== HARD SYNC LEAD ===")
|
||||
s = Score("4/4", bpm=128)
|
||||
p = s.part("sync", instrument="sync_lead",
|
||||
reverb=0.25, reverb_type="plate")
|
||||
p.add("E4", EIGHTH).add("E4", EIGHTH).rest(EIGHTH).add("G4", EIGHTH)
|
||||
p.add("A4", QUARTER).add("G4", EIGHTH).add("E4", EIGHTH)
|
||||
p.add("D4", EIGHTH).rest(EIGHTH).add("E4", EIGHTH).add("G4", EIGHTH)
|
||||
p.add("A4", HALF)
|
||||
p.rest(QUARTER).add("B4", EIGHTH).add("A4", EIGHTH)
|
||||
p.add("G4", QUARTER).add("E4", QUARTER).add("D4", HALF)
|
||||
play_score(s)
|
||||
|
||||
|
||||
# ── Hard Sync Bright ─────────────────────────────────────────────────────────
|
||||
# Higher slave ratio — more harmonics, screaming lead.
|
||||
|
||||
print("\n=== HARD SYNC BRIGHT ===")
|
||||
s = Score("4/4", bpm=138)
|
||||
p = s.part("sync2", instrument="sync_lead_bright",
|
||||
reverb=0.2, reverb_type="plate")
|
||||
p.add("A4", EIGHTH).add("C5", EIGHTH).add("D5", QUARTER)
|
||||
p.rest(EIGHTH).add("E5", EIGHTH).add("D5", EIGHTH).add("C5", EIGHTH)
|
||||
p.add("A4", QUARTER).rest(QUARTER).add("G4", EIGHTH).add("A4", EIGHTH)
|
||||
p.add("C5", HALF)
|
||||
play_score(s)
|
||||
|
||||
|
||||
# ── Ring Mod Bell ────────────────────────────────────────────────────────────
|
||||
# Shimmery, metallic — sparse hits with long reverb tail.
|
||||
|
||||
print("\n=== RING MOD BELL ===")
|
||||
s = Score("4/4", bpm=66)
|
||||
p = s.part("bell", instrument="ring_mod_bell",
|
||||
reverb=0.6, reverb_type="cave", reverb_decay=3.0)
|
||||
p.add("C5", HALF).rest(QUARTER).add("G4", QUARTER)
|
||||
p.rest(HALF).add("E5", HALF)
|
||||
p.add("D5", QUARTER).rest(QUARTER).add("C5", HALF)
|
||||
p.rest(WHOLE)
|
||||
p.add("G4", QUARTER).add("A4", QUARTER).add("C5", HALF)
|
||||
play_score(s)
|
||||
|
||||
|
||||
# ── Ring Mod Metallic ────────────────────────────────────────────────────────
|
||||
# Alien, inharmonic — atonal stabs.
|
||||
|
||||
print("\n=== RING MOD METALLIC ===")
|
||||
s = Score("4/4", bpm=100)
|
||||
p = s.part("metal", instrument="ring_mod_metallic",
|
||||
reverb=0.4, reverb_type="parking_garage", reverb_decay=2.0)
|
||||
p.add("F4", EIGHTH).rest(EIGHTH).add("Ab4", EIGHTH).add("F4", EIGHTH)
|
||||
p.rest(QUARTER).add("Db5", QUARTER).rest(QUARTER)
|
||||
p.add("C5", EIGHTH).add("Ab4", EIGHTH).rest(QUARTER).add("F4", HALF)
|
||||
p.rest(HALF).add("Db5", QUARTER).add("C5", QUARTER)
|
||||
play_score(s)
|
||||
|
||||
|
||||
# ── Wavefold Warm ────────────────────────────────────────────────────────────
|
||||
# Gentle folds — round and musical, like a filtered saw with overtones.
|
||||
|
||||
print("\n=== WAVEFOLD WARM ===")
|
||||
s = Score("4/4", bpm=108)
|
||||
p = s.part("fold", instrument="wavefold_warm",
|
||||
reverb=0.3, reverb_type="plate")
|
||||
p.add("A3", QUARTER).add("C4", QUARTER).add("E4", QUARTER).add("A4", QUARTER)
|
||||
p.add("G4", DOTTED_Q).add("E4", EIGHTH).add("C4", HALF)
|
||||
p.add("D4", QUARTER).add("F4", QUARTER).add("A4", HALF)
|
||||
p.add("G4", WHOLE)
|
||||
play_score(s)
|
||||
|
||||
|
||||
# ── Wavefold Gnarly ──────────────────────────────────────────────────────────
|
||||
# Cranked folds — buzzy, aggressive, with syncopation.
|
||||
|
||||
print("\n=== WAVEFOLD GNARLY ===")
|
||||
s = Score("4/4", bpm=130)
|
||||
p = s.part("gnarly", instrument="wavefold_gnarly",
|
||||
reverb=0.2, reverb_type="spring")
|
||||
p.add("E3", EIGHTH).add("E3", EIGHTH).rest(EIGHTH).add("G3", EIGHTH)
|
||||
p.add("A3", EIGHTH).rest(EIGHTH).add("B3", EIGHTH).add("A3", EIGHTH)
|
||||
p.add("E3", QUARTER).add("G3", EIGHTH).add("A3", EIGHTH).add("B3", QUARTER)
|
||||
p.rest(QUARTER)
|
||||
p.add("E4", EIGHTH).add("D4", EIGHTH).add("B3", QUARTER).add("A3", HALF)
|
||||
play_score(s)
|
||||
|
||||
|
||||
# ── Drift Saw ────────────────────────────────────────────────────────────────
|
||||
# Warm, alive analog saw — the Minimoog pad.
|
||||
|
||||
print("\n=== DRIFT SAW (vintage VCO) ===")
|
||||
s = Score("4/4", bpm=88)
|
||||
p = s.part("drift", instrument="drift_saw",
|
||||
reverb=0.35, reverb_type="taj_mahal", reverb_decay=2.0)
|
||||
p.add("D4", HALF).add("F4", HALF)
|
||||
p.add("A4", DOTTED_Q).add("G4", EIGHTH).add("F4", QUARTER).rest(QUARTER)
|
||||
p.add("D4", QUARTER).add("E4", QUARTER).add("F4", HALF)
|
||||
p.add("D4", WHOLE)
|
||||
play_score(s)
|
||||
|
||||
|
||||
# ── Drift Square ─────────────────────────────────────────────────────────────
|
||||
# Hollow, wobbly — 8-bit with analog soul.
|
||||
|
||||
print("\n=== DRIFT SQUARE ===")
|
||||
s = Score("4/4", bpm=110)
|
||||
p = s.part("dsq", instrument="drift_square",
|
||||
reverb=0.25, reverb_type="plate")
|
||||
p.add("C4", EIGHTH).add("E4", EIGHTH).add("G4", QUARTER).add("E4", QUARTER)
|
||||
p.rest(QUARTER)
|
||||
p.add("A4", EIGHTH).add("G4", EIGHTH).add("E4", QUARTER).add("C4", HALF)
|
||||
p.add("D4", QUARTER).add("F4", EIGHTH).add("G4", EIGHTH).add("A4", HALF)
|
||||
p.add("G4", WHOLE)
|
||||
play_score(s)
|
||||
|
||||
|
||||
# ── Analog Pad ───────────────────────────────────────────────────────────────
|
||||
# Slow, drifting chords — Juno-style lushness.
|
||||
|
||||
print("\n=== ANALOG PAD ===")
|
||||
s = Score("4/4", bpm=70)
|
||||
p = s.part("pad", instrument="analog_pad",
|
||||
reverb=0.5, reverb_type="taj_mahal", reverb_decay=3.0)
|
||||
p.add("A3", WHOLE)
|
||||
p.add("C4", HALF).add("E4", HALF)
|
||||
p.add("F4", WHOLE)
|
||||
p.add("E4", HALF).add("D4", HALF)
|
||||
p.add("C4", WHOLE)
|
||||
play_score(s)
|
||||
|
||||
|
||||
# ── Analog Bass ──────────────────────────────────────────────────────────────
|
||||
# Tight, punchy — Moog bass with filter sweep.
|
||||
|
||||
print("\n=== ANALOG BASS ===")
|
||||
s = Score("4/4", bpm=120)
|
||||
p = s.part("bass", instrument="analog_bass",
|
||||
reverb=0.1, reverb_type="plate")
|
||||
p.add("E2", EIGHTH).add("E2", EIGHTH).rest(EIGHTH).add("G2", EIGHTH)
|
||||
p.add("A2", QUARTER).rest(QUARTER)
|
||||
p.add("E2", EIGHTH).rest(EIGHTH).add("B2", EIGHTH).add("A2", EIGHTH)
|
||||
p.add("G2", QUARTER).add("E2", QUARTER).rest(HALF)
|
||||
p.add("E2", EIGHTH).add("E2", EIGHTH).add("G2", EIGHTH).add("A2", EIGHTH)
|
||||
p.add("B2", QUARTER).add("A2", QUARTER).add("E2", HALF)
|
||||
play_score(s)
|
||||
|
||||
|
||||
print("\nDone!")
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -850,6 +850,56 @@ def gen_synth_ukulele():
|
||||
p.strum(ch, Duration.WHOLE, velocity=72)
|
||||
render("synth_ukulele", score)
|
||||
|
||||
def gen_synth_hard_sync():
|
||||
score = Score("4/4", bpm=120)
|
||||
p = score.part("demo", instrument="sync_lead_bright", volume=0.5)
|
||||
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
|
||||
p.add(n, Duration.QUARTER, velocity=90)
|
||||
render("synth_hard_sync", score)
|
||||
|
||||
|
||||
def gen_synth_ring_mod():
|
||||
score = Score("4/4", bpm=90)
|
||||
p = score.part("demo", instrument="ring_mod_bell", volume=0.5)
|
||||
for n in ["C5", "E5", "G5", "C6", "G5", "E5", "C5", "E5"]:
|
||||
p.add(n, Duration.QUARTER, velocity=80)
|
||||
render("synth_ring_mod", score)
|
||||
|
||||
|
||||
def gen_synth_wavefold():
|
||||
score = Score("4/4", bpm=110)
|
||||
p = score.part("demo", instrument="wavefold_warm", volume=0.5)
|
||||
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
|
||||
p.add(n, Duration.QUARTER, velocity=85)
|
||||
render("synth_wavefold", score)
|
||||
|
||||
|
||||
def gen_synth_drift():
|
||||
score = Score("4/4", bpm=90)
|
||||
p = score.part("demo", instrument="drift_saw", volume=0.5, reverb=0.35,
|
||||
reverb_type="taj_mahal")
|
||||
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
|
||||
p.add(n, Duration.HALF, velocity=75)
|
||||
render("synth_drift", score)
|
||||
|
||||
|
||||
def gen_synth_karplus():
|
||||
score = Score("4/4", bpm=100)
|
||||
p = score.part("demo", synth="pluck_synth", envelope="none",
|
||||
volume=0.5, reverb=0.2)
|
||||
for n in ["C4", "E4", "G4", "C5", "G4", "E4", "C4", "E4"]:
|
||||
p.add(n, Duration.QUARTER, velocity=85)
|
||||
render("synth_karplus", score)
|
||||
|
||||
|
||||
def gen_synth_mellotron():
|
||||
score = Score("4/4", bpm=80)
|
||||
p = score.part("demo", instrument="mellotron_flute", volume=0.5)
|
||||
for n in ["C4", "E4", "G4", "C5"]:
|
||||
p.add(n, Duration.WHOLE, velocity=75)
|
||||
render("synth_mellotron", score)
|
||||
|
||||
|
||||
def gen_synth_granular():
|
||||
score = Score("4/4", bpm=80)
|
||||
p = score.part("demo", instrument="granular_pad", volume=0.5, reverb=0.4)
|
||||
@@ -1139,6 +1189,12 @@ GENERATORS = [
|
||||
gen_synth_banjo,
|
||||
gen_synth_mandolin,
|
||||
gen_synth_ukulele,
|
||||
gen_synth_hard_sync,
|
||||
gen_synth_ring_mod,
|
||||
gen_synth_wavefold,
|
||||
gen_synth_drift,
|
||||
gen_synth_karplus,
|
||||
gen_synth_mellotron,
|
||||
gen_synth_granular,
|
||||
gen_synth_crotales,
|
||||
gen_synth_tingsha,
|
||||
|
||||
@@ -503,9 +503,16 @@ are standard arranging techniques for spreading chord tones across registers:
|
||||
>>> cmaj7 = Chord.from_symbol("Cmaj7")
|
||||
>>> cmaj7.close_voicing()
|
||||
<Chord C major 7th>
|
||||
>>> cmaj7.open_voicing()
|
||||
<Chord C major 7th>
|
||||
>>> cmaj7.drop2()
|
||||
<Chord C major 7th>
|
||||
|
||||
``open_voicing()`` takes the close voicing and raises every other
|
||||
non-root tone by an octave, spreading the chord across two octaves.
|
||||
The result is a wider, more spacious sound — common in orchestral
|
||||
writing and piano ballads where you want the harmony to breathe.
|
||||
|
||||
Chord Extensions
|
||||
----------------
|
||||
|
||||
@@ -596,6 +603,23 @@ music that doesn't follow traditional harmony, this is the tool.
|
||||
Major and minor triads share the same prime form — they're inversions
|
||||
of each other in pitch class space.
|
||||
|
||||
The **normal form** is the intermediate step — the most compact ascending
|
||||
arrangement of pitch classes before transposition. It preserves the
|
||||
actual pitch classes (not transposed to 0), so it tells you which
|
||||
specific notes are in the set:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G").normal_form
|
||||
(0, 4, 7)
|
||||
|
||||
>>> Chord.from_tones("A", "C", "E").normal_form
|
||||
(9, 0, 4)
|
||||
|
||||
Normal form keeps the original pitch classes; prime form transposes to 0
|
||||
for comparison. Use ``normal_form`` when you care about which notes,
|
||||
``prime_form`` when you care about the abstract shape.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_tones("C", "E", "G").forte_number
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
Nashville Numbers, Blues Scales, and Tablature
|
||||
===============================================
|
||||
|
||||
Three tools that work together: the Nashville number system for writing
|
||||
chord charts, blues scales for improvisation, and tablature for seeing
|
||||
where to put your fingers. This guide covers all three and shows how
|
||||
they connect.
|
||||
|
||||
The Nashville Number System
|
||||
---------------------------
|
||||
|
||||
The `Nashville number system <https://en.wikipedia.org/wiki/Nashville_Number_System>`_
|
||||
replaces chord names with Arabic numerals (1, 2, 3...) so that a chart
|
||||
works in **any key**. It's the standard chart format in Nashville
|
||||
recording studios — a session musician can read a number chart and
|
||||
transpose on the fly without rewriting anything.
|
||||
|
||||
The idea is simple: each number refers to a **scale degree**. In any
|
||||
major key, 1 is the tonic chord, 4 is the subdominant, 5 is the
|
||||
dominant, and so on. The chord quality (major, minor, diminished) is
|
||||
determined by the key — you don't need to write it out.
|
||||
|
||||
In C major::
|
||||
|
||||
1 = C major 5 = G major
|
||||
2 = D minor 6 = A minor
|
||||
3 = E minor 7 = B diminished
|
||||
4 = F major
|
||||
|
||||
In G major::
|
||||
|
||||
1 = G major 5 = D major
|
||||
2 = A minor 6 = E minor
|
||||
3 = B minor 7 = F# diminished
|
||||
4 = C major
|
||||
|
||||
Same numbers, different key, different chords — but the same harmonic
|
||||
relationships.
|
||||
|
||||
Using Nashville Numbers in PyTheory
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Both :class:`~pytheory.scales.Key` and :class:`~pytheory.scales.TonedScale`
|
||||
support the ``nashville()`` method:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> [c.identify() for c in key.nashville(1, 4, 5, 1)]
|
||||
['C major', 'F major', 'G major', 'C major']
|
||||
|
||||
>>> # Same progression, different key — just change the Key
|
||||
>>> key_g = Key("G", "major")
|
||||
>>> [c.identify() for c in key_g.nashville(1, 4, 5, 1)]
|
||||
['G major', 'C major', 'D major', 'G major']
|
||||
|
||||
Nashville numbers and Roman numerals produce the same result — they're
|
||||
two notations for the same concept:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> key = Key("G", "major")
|
||||
>>> nash = [c.identify() for c in key.nashville(1, 5, 6, 4)]
|
||||
>>> roman = [c.identify() for c in key.progression("I", "V", "vi", "IV")]
|
||||
>>> nash == roman
|
||||
True
|
||||
|
||||
Seventh Chords
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Suffix ``"7"`` to get seventh chords — essential for jazz and blues
|
||||
charts:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> key = Key("C", "major")
|
||||
>>> [c.identify() for c in key.nashville("17", "47", "57")]
|
||||
['C major 7th', 'F major 7th', 'G dominant 7th']
|
||||
|
||||
Nashville vs. Roman Numerals
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When should you use which?
|
||||
|
||||
- **Nashville numbers** — faster to type, easier to read at a glance,
|
||||
standard in studio sessions. Use ``key.nashville(1, 4, 5, 1)``.
|
||||
- **Roman numerals** — encode chord quality (uppercase = major,
|
||||
lowercase = minor), standard in theory textbooks. Use
|
||||
``key.progression("I", "IV", "V", "I")``.
|
||||
|
||||
Both are fully supported. Use whichever fits your workflow.
|
||||
|
||||
Blues Scales
|
||||
------------
|
||||
|
||||
The `blues scale <https://en.wikipedia.org/wiki/Blues_scale>`_ is a
|
||||
six-note scale built from the minor pentatonic plus one chromatic
|
||||
passing tone — the **blue note** (flat 5th). That single added note
|
||||
gives the blues its tension and character.
|
||||
|
||||
The blues system in PyTheory includes several related scales:
|
||||
|
||||
==================== ===== ==================================
|
||||
Scale Notes Character
|
||||
==================== ===== ==================================
|
||||
minor pentatonic 5 Foundation of rock and blues soloing
|
||||
major pentatonic 5 Bright, country, pop
|
||||
blues 6 Minor pentatonic + blue note (b5)
|
||||
major blues 6 Major pentatonic + blue note (b3)
|
||||
dominant 7 Mixolydian — dominant 7th sound
|
||||
minor 7 Dorian-like — minor with natural 6th
|
||||
==================== ===== ==================================
|
||||
|
||||
Building Blues Scales
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Use ``system="blues"`` when creating a :class:`~pytheory.scales.TonedScale`:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import TonedScale
|
||||
|
||||
>>> c = TonedScale(tonic="C4", system="blues")
|
||||
|
||||
>>> c["minor pentatonic"].note_names
|
||||
['C', 'Eb', 'F', 'G', 'Bb', 'C']
|
||||
|
||||
>>> c["blues"].note_names
|
||||
['C', 'Eb', 'F', 'Gb', 'G', 'Bb', 'C']
|
||||
|
||||
>>> c["major pentatonic"].note_names
|
||||
['C', 'D', 'E', 'G', 'A', 'C']
|
||||
|
||||
>>> c["major blues"].note_names
|
||||
['C', 'D', 'Eb', 'E', 'G', 'A', 'C']
|
||||
|
||||
The Anatomy of a Blues Scale
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The blues scale in C::
|
||||
|
||||
C Eb F Gb G Bb C
|
||||
1 b3 4 b5 5 b7 8
|
||||
|
||||
Root ──┐
|
||||
├── minor 3rd (3 semitones)
|
||||
├── perfect 4th (5 semitones)
|
||||
├── diminished 5th (6 semitones) ← the "blue note"
|
||||
├── perfect 5th (7 semitones)
|
||||
├── minor 7th (10 semitones)
|
||||
└── octave (12 semitones)
|
||||
|
||||
The blue note (Gb/F#) sits between the 4th and 5th — a dissonant,
|
||||
unstable pitch that resolves up or down. It's what makes blues sound
|
||||
like blues.
|
||||
|
||||
The 12-Bar Blues
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The `12-bar blues <https://en.wikipedia.org/wiki/Twelve-bar_blues>`_ is
|
||||
the most important chord progression in American music. It uses the
|
||||
Nashville numbers 1, 4, and 5::
|
||||
|
||||
| 1 | 1 | 1 | 1 |
|
||||
| 4 | 4 | 1 | 1 |
|
||||
| 5 | 4 | 1 | 5 |
|
||||
|
||||
In the key of A:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Key
|
||||
|
||||
>>> key = Key("A", "major")
|
||||
>>> bars = key.nashville(1,1,1,1, 4,4,1,1, 5,4,1,5)
|
||||
>>> [c.identify() for c in bars]
|
||||
['A major', 'A major', 'A major', 'A major', 'D major', 'D major', 'A major', 'A major', 'E major', 'D major', 'A major', 'E major']
|
||||
|
||||
For an authentic blues sound, use dominant 7th chords:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> bars_7 = key.nashville("17","17","17","17", "47","47","17","17", "57","47","17","57")
|
||||
>>> [c.identify() for c in bars_7]
|
||||
['A major 7th', 'A major 7th', 'A major 7th', 'A major 7th', 'D major 7th', 'D major 7th', 'A major 7th', 'A major 7th', 'E dominant 7th', 'D major 7th', 'A major 7th', 'E dominant 7th']
|
||||
|
||||
Or use the built-in named progression:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> key = Key("A", "major")
|
||||
>>> blues = key.common_progressions()["12-bar blues"]
|
||||
>>> [c.identify() for c in blues]
|
||||
['A major', 'A major', 'A major', 'A major', 'D major', 'D major', 'A major', 'A major', 'E major', 'D major', 'A major', 'E major']
|
||||
|
||||
Blues Scale on the Fretboard
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Visualize the blues scale on guitar to see the patterns:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard, TonedScale
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> blues = TonedScale(tonic="A4", system="blues")["blues"]
|
||||
>>> print(fb.scale_diagram(blues, frets=12))
|
||||
0 1 2 3 4 5 6 7 8 9 10 11 12
|
||||
E| - | - | - | - | - | A | - | - | C | - | D | Eb| E |
|
||||
B| - | - | - | D | Eb| E | - | - | - | - | A | - | - |
|
||||
G| - | - | A | - | - | C | - | D | Eb| E | - | - | - |
|
||||
D| - | - | - | - | A | - | - | C | - | D | Eb| E | - |
|
||||
A| A | - | - | C | - | D | Eb| E | - | - | - | - | A |
|
||||
E| - | - | - | - | - | A | - | - | C | - | D | Eb| E |
|
||||
|
||||
The minor pentatonic (same scale without the Eb) is the most-played
|
||||
scale in rock guitar. Add the blue note and you have the full blues
|
||||
scale — the same shapes, one extra fret.
|
||||
|
||||
Tablature
|
||||
---------
|
||||
|
||||
`Tablature <https://en.wikipedia.org/wiki/Tablature>`_ (tab) shows
|
||||
**where to put your fingers** rather than what notes to play. Each line
|
||||
represents a string; numbers indicate fret positions. PyTheory generates
|
||||
tabs at three levels:
|
||||
|
||||
1. **Chord tabs** — single chord fingerings
|
||||
2. **Part tabs** — full melody/sequence notation
|
||||
3. **Score tabs** — extract a part from a multi-part score
|
||||
|
||||
Chord Tablature
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Get the tab for any chord on any instrument:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from pytheory import Fretboard
|
||||
|
||||
>>> fb = Fretboard.guitar()
|
||||
>>> print(fb.tab("C"))
|
||||
C major
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--0--
|
||||
D|--2--
|
||||
A|--3--
|
||||
E|--x--
|
||||
|
||||
>>> print(fb.tab("Am"))
|
||||
A minor
|
||||
e|--0--
|
||||
B|--1--
|
||||
G|--2--
|
||||
D|--2--
|
||||
A|--0--
|
||||
E|--x--
|
||||
|
||||
>>> print(fb.tab("E7"))
|
||||
E dominant 7th
|
||||
e|--0--
|
||||
B|--0--
|
||||
G|--1--
|
||||
D|--0--
|
||||
A|--2--
|
||||
E|--0--
|
||||
|
||||
Works with any instrument:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> uke = Fretboard.ukulele()
|
||||
>>> print(uke.tab("C"))
|
||||
C major
|
||||
A|--3--
|
||||
E|--0--
|
||||
C|--0--
|
||||
G|--0--
|
||||
|
||||
Reading Tab Notation
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
e|--0-- ← open string (don't fret, just pluck)
|
||||
B|--1-- ← press fret 1
|
||||
G|--0-- ← open string
|
||||
D|--2-- ← press fret 2
|
||||
A|--3-- ← press fret 3
|
||||
E|--x-- ← muted (don't play this string)
|
||||
|
||||
- Each line is a string (highest pitch at top, lowest at bottom)
|
||||
- Numbers are fret positions (0 = open, 1-24 = fretted)
|
||||
- ``x`` means the string is muted / not played
|
||||
- ``|`` marks measure boundaries in sequence tabs
|
||||
|
||||
Part Tablature
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Generate tab from a composed part using ``to_tab()``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Key, Duration
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
lead = score.part("lead", synth="saw")
|
||||
|
||||
# A simple blues lick
|
||||
for note in ["A4", "C5", "D5", "Eb5", "E5", "G5", "A5"]:
|
||||
lead.add(note, Duration.QUARTER)
|
||||
|
||||
print(lead.to_tab())
|
||||
|
||||
This outputs standard ASCII tab with measure lines, mapping each note
|
||||
to the most playable string and fret position.
|
||||
|
||||
Tuning Options
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The ``to_tab()`` method supports multiple tunings:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Standard guitar (default)
|
||||
lead.to_tab(tuning="guitar")
|
||||
|
||||
# 4-string bass
|
||||
lead.to_tab(tuning="bass")
|
||||
|
||||
# Drop D guitar
|
||||
lead.to_tab(tuning="drop_d")
|
||||
|
||||
# Any Fretboard object — use any of the 25+ instrument presets
|
||||
from pytheory import Fretboard
|
||||
lead.to_tab(tuning=Fretboard.mandolin())
|
||||
lead.to_tab(tuning=Fretboard.banjo())
|
||||
|
||||
# Custom tuning as MIDI note numbers (low string first)
|
||||
lead.to_tab(tuning=[40, 45, 50, 55, 59, 64])
|
||||
|
||||
Score Tablature
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Extract tab from a multi-part score:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
rhythm = score.part("rhythm", synth="saw")
|
||||
lead = score.part("lead", synth="triangle")
|
||||
bass = score.part("bass", synth="sine")
|
||||
|
||||
# ... compose parts ...
|
||||
|
||||
# Tab the lead part
|
||||
print(score.to_tab("lead"))
|
||||
|
||||
# Tab the first non-drum part (if no name given)
|
||||
print(score.to_tab())
|
||||
|
||||
# Bass tab
|
||||
print(score.to_tab("bass", tuning="bass"))
|
||||
|
||||
Putting It All Together
|
||||
-----------------------
|
||||
|
||||
Here's a complete example that uses all three features — Nashville
|
||||
numbers for the chord progression, the blues scale for the melody, and
|
||||
tab export to see the fingering:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Key, TonedScale, Fretboard, Score, Duration
|
||||
|
||||
# 1. Nashville numbers for the progression
|
||||
key = Key("A", "major")
|
||||
chords = key.nashville(1, 1, 1, 1, 4, 4, 1, 1, 5, 4, 1, 5)
|
||||
|
||||
# 2. Blues scale for the melody
|
||||
blues = TonedScale(tonic="A4", system="blues")["blues"]
|
||||
|
||||
# 3. Compose a score
|
||||
score = Score("4/4", bpm=120)
|
||||
rhythm = score.part("rhythm", synth="saw", envelope="pad")
|
||||
lead = score.part("lead", synth="triangle", envelope="pluck")
|
||||
|
||||
for chord in chords:
|
||||
rhythm.add(chord, Duration.WHOLE)
|
||||
|
||||
for note_name in blues.note_names[:-1]: # walk up the scale
|
||||
lead.add(f"{note_name}4", Duration.HALF)
|
||||
|
||||
# 4. See it as tablature
|
||||
print(lead.to_tab())
|
||||
|
||||
# 5. See the scale on the fretboard
|
||||
fb = Fretboard.guitar()
|
||||
print(fb.scale_diagram(blues, frets=12))
|
||||
|
||||
Nashville numbers tell you *what chords to play*. The blues scale tells you *what notes to solo with*. Tablature tells you *where to put your fingers*. Together, they're everything you need to play the blues.
|
||||
+173
-7
@@ -3,19 +3,21 @@ Playback and Export
|
||||
|
||||
This is the output layer. You've built your theory, composed your
|
||||
arrangement, shaped your sounds -- now you need to hear it. PyTheory
|
||||
gives you three ways to get your music out: speakers, WAV files, and
|
||||
MIDI files.
|
||||
gives you four ways to get your music out: speakers, WAV files, MIDI
|
||||
files, and sheet music.
|
||||
|
||||
Use **speakers** for immediate feedback while you're sketching and
|
||||
experimenting. Use **WAV export** when you want to share actual audio
|
||||
-- post it, send it, drop it into a video. Use **MIDI export** when you
|
||||
want to bring your sketch into a real DAW and finish it with
|
||||
professional instruments, mixing, and mastering. Each output serves a
|
||||
different stage of the creative process.
|
||||
professional instruments, mixing, and mastering. Use **ABC notation
|
||||
export** when you want sheet music -- rendered in the browser or shared
|
||||
as plain text. Each output serves a different stage of the creative
|
||||
process.
|
||||
|
||||
PyTheory can play audio through your speakers, save to WAV, or export
|
||||
to MIDI. Everything is synthesized from waveforms -- no samples or
|
||||
external audio files needed.
|
||||
PyTheory can play audio through your speakers, save to WAV, export to
|
||||
MIDI, or generate sheet music as ABC notation. Everything is synthesized
|
||||
from waveforms -- no samples or external audio files needed.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -44,6 +46,22 @@ Optional parameters for synth, envelope, and temperament:
|
||||
play(Tone.from_string("C4"), synth=Synth.SAW, envelope=Envelope.PLUCK, t=1_000)
|
||||
play(Tone.from_string("C4"), temperament="pythagorean", t=1_000)
|
||||
|
||||
Synth-specific parameters are passed through as keyword arguments:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Mellotron with flute tape
|
||||
play(Tone.from_string("C4"), synth=Synth.MELLOTRON, tape="choir", t=2_000)
|
||||
|
||||
# Hard sync with custom slave ratio
|
||||
play(Tone.from_string("C4"), synth=Synth.HARD_SYNC, slave_ratio=2.5)
|
||||
|
||||
# Wavefolding with 4 folds
|
||||
play(Tone.from_string("C4"), synth=Synth.WAVEFOLD, folds=4.0)
|
||||
|
||||
# Drift oscillator with square shape
|
||||
play(Tone.from_string("C4"), synth=Synth.DRIFT, shape="square")
|
||||
|
||||
play_score() -- Full Arrangements
|
||||
---------------------------------
|
||||
|
||||
@@ -155,6 +173,154 @@ Score-based export (with time signature, tempo, and parts):
|
||||
score.add(chord, Duration.WHOLE)
|
||||
score.save_midi("progression.mid")
|
||||
|
||||
to_abc() -- ABC Notation / Sheet Music
|
||||
---------------------------------------
|
||||
|
||||
ABC notation is a human-readable text format for music that tools can
|
||||
turn into staff notation and MIDI. It's widely used for folk tunes,
|
||||
lead sheets, and quick sketches. PyTheory can export any Score as ABC
|
||||
notation -- and optionally wrap it in an HTML page that renders
|
||||
sheet music right in the browser using `abcjs <https://www.abcjs.net/>`_.
|
||||
|
||||
Basic export:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Duration, Key
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
lead = score.part("lead")
|
||||
for chord in Key("C", "major").progression("I", "V", "vi", "IV"):
|
||||
lead.add(chord, Duration.WHOLE)
|
||||
|
||||
print(score.to_abc(title="Pop Chords", key="C"))
|
||||
|
||||
Output:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
X:1
|
||||
T:Pop Chords
|
||||
M:4/4
|
||||
Q:1/4=120
|
||||
L:1/8
|
||||
K:C
|
||||
[CEG]8 | [GBd]8 | [Ace]8 | [FAc]8 |
|
||||
|
||||
Open sheet music in the browser with ``html=True``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
html = score.to_abc(title="Pop Chords", key="C", html=True)
|
||||
|
||||
with open("chords.html", "w") as f:
|
||||
f.write(html)
|
||||
|
||||
import webbrowser
|
||||
webbrowser.open("chords.html")
|
||||
|
||||
This generates a self-contained HTML page with an embedded
|
||||
``<script>`` tag that loads abcjs from a CDN and renders the notation
|
||||
as SVG -- no build steps, no dependencies, just open the file.
|
||||
|
||||
Multi-part scores automatically get ``V:`` (voice) directives so each
|
||||
instrument appears on its own staff. Bass parts (average note below C4)
|
||||
get bass clef automatically. Drum-only parts are skipped. Notes longer
|
||||
than one measure are split into tied notes across barlines.
|
||||
|
||||
Parameters:
|
||||
|
||||
- **title** -- Tune title for the ``T:`` header (default ``"Untitled"``).
|
||||
- **key** -- ABC key signature string (default ``"C"``). Use ``"Am"`` for
|
||||
A minor, ``"Bb"`` for B-flat major, ``"F#m"`` for F-sharp minor, etc.
|
||||
- **html** -- If ``True``, return a full HTML document instead of raw ABC
|
||||
(default ``False``).
|
||||
|
||||
to_lilypond() -- LilyPond Export
|
||||
---------------------------------
|
||||
|
||||
`LilyPond <https://lilypond.org/>`_ is the gold standard for
|
||||
publication-quality music engraving. ``to_lilypond()`` generates
|
||||
complete LilyPond source files that you can compile to PDF:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=120)
|
||||
lead = score.part("lead")
|
||||
for note in ["C4", "D4", "E4", "F4"]:
|
||||
lead.add(note, Duration.QUARTER)
|
||||
|
||||
ly = score.to_lilypond(title="My Score", key="C", mode="major")
|
||||
|
||||
with open("score.ly", "w") as f:
|
||||
f.write(ly)
|
||||
|
||||
Then compile with ``lilypond score.ly`` to get a PDF. Multi-part scores
|
||||
get separate staves in a ``StaffGroup``, bass clef is auto-detected,
|
||||
and long notes are split with ties across barlines.
|
||||
|
||||
Parameters:
|
||||
|
||||
- **title** -- Title for the ``\header`` block (default ``"Untitled"``).
|
||||
- **key** -- Key signature root (default ``"C"``). Use note names like
|
||||
``"Bb"``, ``"F#"``, ``"Eb"``.
|
||||
- **mode** -- LilyPond mode string (default ``"major"``). Use ``"minor"``
|
||||
for minor keys.
|
||||
|
||||
to_musicxml() -- MusicXML Export
|
||||
---------------------------------
|
||||
|
||||
MusicXML is the interchange format for notation software. Export your
|
||||
score and open it in MuseScore, Sibelius, Finale, Dorico, or any
|
||||
other notation app:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
xml = score.to_musicxml(title="My Score")
|
||||
|
||||
with open("score.musicxml", "w") as f:
|
||||
f.write(xml)
|
||||
|
||||
The output is a complete MusicXML 4.0 partwise document with proper
|
||||
time signatures, tempo markings, clef detection, tied notes across
|
||||
barlines, and chord notation. No external dependencies needed.
|
||||
|
||||
to_tab() -- Guitar/Bass Tablature
|
||||
-----------------------------------
|
||||
|
||||
Generate ASCII tablature from any Part or Score:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part("lead")
|
||||
lead.add("E4", Duration.QUARTER)
|
||||
lead.add("B3", Duration.QUARTER)
|
||||
lead.add("G3", Duration.QUARTER)
|
||||
lead.add("D3", Duration.QUARTER)
|
||||
|
||||
print(lead.to_tab())
|
||||
|
||||
Output::
|
||||
|
||||
e|---0---------|
|
||||
B|------0------|
|
||||
G|---------0---|
|
||||
D|------------0|
|
||||
A|-------------|
|
||||
E|-------------|
|
||||
|
||||
Works on Score too -- it picks the first melodic part automatically:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(score.to_tab()) # auto-pick part
|
||||
print(score.to_tab(part_name="bass")) # specific part
|
||||
print(score.to_tab(tuning="bass")) # 4-string bass tab
|
||||
print(score.to_tab(tuning="drop_d")) # drop D guitar
|
||||
|
||||
Supports ``"guitar"`` (6-string standard), ``"bass"`` (4-string),
|
||||
``"drop_d"``, or a custom list of MIDI note numbers for any tuning.
|
||||
|
||||
play_pattern() -- Drum Patterns
|
||||
-------------------------------
|
||||
|
||||
|
||||
+27
-1
@@ -269,6 +269,23 @@ easy:
|
||||
['G major', 'A minor', 'B minor', 'C major', 'D major', 'E minor', 'F# diminished']
|
||||
>>> key.seventh_chords
|
||||
['G major 7th', 'A minor 7th', 'B minor 7th', 'C major 7th', 'D dominant 7th', 'E minor 7th', 'F# half-diminished 7th']
|
||||
|
||||
Build a seventh chord on any individual degree with ``seventh()``:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> key.seventh(0) # I7
|
||||
G major 7th
|
||||
>>> key.seventh(4) # V7
|
||||
D dominant 7th
|
||||
>>> key.seventh(6) # vii7
|
||||
F# half-diminished 7th
|
||||
|
||||
This is the single-degree version of ``seventh_chords`` — useful when
|
||||
you need one specific chord rather than the full list.
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Key.detect("C", "E", "G", "A", "D")
|
||||
C major
|
||||
|
||||
@@ -440,7 +457,16 @@ alternative scales to improvise over:
|
||||
>>> Scale.recommend("C", "Eb", "F", "Gb", "G", "Bb", top=3)
|
||||
[('C', 'blues', 1.0), ...]
|
||||
|
||||
Chromatic scales are deprioritized since they match everything.
|
||||
How it works: ``recommend()`` tests your notes against every scale in
|
||||
every key (all 12 tonics times all scale types in the Western system).
|
||||
Each candidate is scored using ``fitness()`` — the fraction of your notes
|
||||
that belong to that scale (1.0 = perfect match). Results are ranked by
|
||||
fitness, with chromatic scales deprioritized since they match everything.
|
||||
Scales whose length is closer to the number of input notes are preferred
|
||||
when fitness scores tie.
|
||||
|
||||
Returns a list of ``(tonic, scale_name, fitness)`` tuples. Pass ``top=``
|
||||
to control how many results you get back (default 5).
|
||||
|
||||
Parallel Modes
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
+181
-3
@@ -1,7 +1,7 @@
|
||||
Synthesizers
|
||||
============
|
||||
|
||||
PyTheory includes 41 built-in waveforms and 10 ADSR envelope presets.
|
||||
PyTheory includes 56 built-in waveforms and 10 ADSR envelope presets.
|
||||
Every sound is generated from scratch -- no samples or external audio
|
||||
files needed.
|
||||
|
||||
@@ -249,6 +249,130 @@ produces a natural chorus/vibrato effect built into the waveform itself.
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_pwm_fast.wav" type="audio/wav"></audio>
|
||||
|
||||
Analog Synthesis
|
||||
----------------
|
||||
|
||||
These waveforms model the behavior of real analog hardware — the
|
||||
imperfections, interactions, and nonlinearities that make a room full
|
||||
of vintage synths sound so much more alive than a room full of VSTs.
|
||||
Each one is a different approach to the same question: how do you make
|
||||
a digital oscillator sound like it has a soul?
|
||||
|
||||
Hard Sync
|
||||
~~~~~~~~~
|
||||
|
||||
A "slave" oscillator is forced to restart its cycle every time a
|
||||
"master" oscillator completes one. The abrupt restart creates bright
|
||||
formant peaks that sweep as the slave ratio changes. This is THE sound
|
||||
of the Prophet-5, Moog Prodigy, and every screaming analog lead since
|
||||
1978.
|
||||
|
||||
**Use for:** aggressive leads, formant sweeps, cutting solos.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
lead = score.part("lead", synth="hard_sync", envelope="pluck")
|
||||
|
||||
# Higher slave ratio = more harmonics, brighter
|
||||
from pytheory import play, Synth, Tone
|
||||
play(Tone.from_string("C4"), synth=Synth.HARD_SYNC, slave_ratio=2.5)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_hard_sync.wav" type="audio/wav"></audio>
|
||||
|
||||
Ring Modulation
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Two oscillators multiplied together, producing sum and difference
|
||||
frequencies. Unlike FM, ring mod outputs only sidebands — no carrier
|
||||
or modulator fundamental. The result is metallic, bell-like, and often
|
||||
inharmonic. Classic Dalek voice, Stockhausen, and every sci-fi
|
||||
soundtrack.
|
||||
|
||||
**Use for:** metallic bells, alien textures, inharmonic percussion.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
bells = score.part("bells", instrument="ring_mod_bell",
|
||||
reverb=0.5, reverb_type="cave")
|
||||
|
||||
# Non-integer ratios = more inharmonic
|
||||
play(Tone.from_string("C4"), synth=Synth.RING_MOD, mod_ratio=2.1)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_ring_mod.wav" type="audio/wav"></audio>
|
||||
|
||||
Wavefolding
|
||||
~~~~~~~~~~~
|
||||
|
||||
The heart of west coast synthesis (Buchla, Make Noise, Verbos). A sine
|
||||
wave is amplified past ±1.0, then "folded" — the overflow bounces back
|
||||
instead of clipping. Each fold adds new harmonic pairs. At low fold
|
||||
counts it's warm and round; crank it up and it gets buzzy, gnarly, and
|
||||
alive.
|
||||
|
||||
This sounds completely different from subtractive synthesis — instead of
|
||||
*removing* harmonics with a filter, you're *generating* them by shaping
|
||||
the wave. Pairs beautifully with a lowpass filter after the fold.
|
||||
|
||||
**Use for:** complex leads, evolving textures, west coast basslines.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Warm, musical folding
|
||||
warm = score.part("fold", instrument="wavefold_warm")
|
||||
|
||||
# Cranked and aggressive
|
||||
gnarly = score.part("gnarly", instrument="wavefold_gnarly")
|
||||
|
||||
# Direct control over fold amount
|
||||
play(Tone.from_string("C4"), synth=Synth.WAVEFOLD, folds=3.0)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_wavefold.wav" type="audio/wav"></audio>
|
||||
|
||||
Drift Oscillator
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Real analog oscillators are never perfectly stable. Capacitor charging,
|
||||
thermal variations, and component tolerances make the pitch wander
|
||||
slightly. This is what makes a Minimoog sound "fat" and a VST sound
|
||||
"thin" — the constant micro-motion of imperfect hardware.
|
||||
|
||||
The drift oscillator models slow pitch drift (< 1 Hz wander), fast
|
||||
jitter (per-cycle randomness), a soft analog noise floor, and slightly
|
||||
rounded waveform edges. It turns any basic shape into something that
|
||||
breathes.
|
||||
|
||||
**Use for:** analog-style pads, warm basses, vintage leads, any voice
|
||||
that needs to feel "alive."
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Vintage Minimoog-style saw
|
||||
pad = score.part("pad", instrument="drift_saw",
|
||||
reverb=0.35, reverb_type="taj_mahal")
|
||||
|
||||
# Hollow square with analog wobble
|
||||
sq = score.part("sq", instrument="drift_square")
|
||||
|
||||
# Control the shape and instability directly
|
||||
play(Tone.from_string("C4"), synth=Synth.DRIFT,
|
||||
shape="triangle", drift_amount=0.25)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_drift.wav" type="audio/wav"></audio>
|
||||
|
||||
Drift amount controls how unstable the oscillator is:
|
||||
|
||||
- **0.05** = studio-grade (Sequential, Oberheim)
|
||||
- **0.15** = classic vintage (Minimoog, ARP) — the default
|
||||
- **0.30** = barely-holding-it-together (old SH-101)
|
||||
|
||||
ADSR Envelopes
|
||||
--------------
|
||||
|
||||
@@ -406,6 +530,10 @@ It sounds genuinely like a real guitar, harp, or koto.
|
||||
guitar = score.part("guitar", synth="pluck_synth")
|
||||
harp = score.part("harp", instrument="harp") # uses pluck_synth
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_karplus.wav" type="audio/wav"></audio>
|
||||
|
||||
Hammond Organ
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -440,11 +568,11 @@ Dedicated Instrument Synths
|
||||
--------------------------
|
||||
|
||||
Beyond the classic and physical modeling waveforms, PyTheory includes
|
||||
31 dedicated instrument synths. Each one uses tailored synthesis
|
||||
36 dedicated instrument synths. Each one uses tailored synthesis
|
||||
techniques -- additive harmonics, formant shaping, body resonance
|
||||
modeling, and specialized envelopes -- to capture the character of a
|
||||
specific acoustic instrument. These are the waveforms that bring the
|
||||
total count to 41.
|
||||
total count to 56.
|
||||
|
||||
Piano Synth
|
||||
~~~~~~~~~~~
|
||||
@@ -495,6 +623,42 @@ picked up by an electrostatic pickup. More nasal, reedy, and biting
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_wurlitzer.wav" type="audio/wav"></audio>
|
||||
|
||||
Mellotron
|
||||
~~~~~~~~~
|
||||
|
||||
The original "sampler" — a 1960s keyboard where each key triggers a
|
||||
strip of magnetic tape with a pre-recorded instrument. The mechanical
|
||||
tape transport gives it a haunted, lo-fi quality that no digital
|
||||
emulation fully captures: pitch wobbles from uneven capstan speed,
|
||||
bandwidth limited to 300 Hz–6 kHz (like a worn cassette), soft tape
|
||||
saturation, and tapes that physically run out after 8 seconds.
|
||||
|
||||
The Mellotron defined the sound of *Strawberry Fields Forever*,
|
||||
*Stairway to Heaven*, and every prog rock record from 1969–1977.
|
||||
|
||||
Three tape banks are available via the ``tape`` parameter:
|
||||
|
||||
- ``"strings"`` (default) — the iconic MkII string section
|
||||
- ``"flute"`` — breathy, haunting solo flute
|
||||
- ``"choir"`` — ghostly vocal pad
|
||||
|
||||
**Use for:** prog rock, haunted textures, vintage orchestral color.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Use instrument presets (includes reverb)
|
||||
strings = score.part("strings", instrument="mellotron_strings")
|
||||
flute = score.part("flute", instrument="mellotron_flute")
|
||||
choir = score.part("choir", instrument="mellotron_choir")
|
||||
|
||||
# Or select the tape directly
|
||||
from pytheory import play, Synth, Tone
|
||||
play(Tone.from_string("C4"), synth=Synth.MELLOTRON, tape="flute", t=3000)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_mellotron.wav" type="audio/wav"></audio>
|
||||
|
||||
Vibraphone Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -1127,6 +1291,12 @@ bagpipe, singing_bowl, singing_bowl_ring, tingsha
|
||||
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass,
|
||||
granular_pad, granular_texture, vocal, choir
|
||||
|
||||
**Mellotron**: mellotron, mellotron_strings, mellotron_flute, mellotron_choir
|
||||
|
||||
**Analog**: sync_lead, sync_lead_bright, ring_mod_bell, ring_mod_metallic,
|
||||
wavefold_warm, wavefold_gnarly, drift_saw, drift_square, analog_pad,
|
||||
analog_bass
|
||||
|
||||
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells,
|
||||
timpani, crotales
|
||||
|
||||
@@ -1169,3 +1339,11 @@ Some practical combos worth memorizing:
|
||||
long reverb and you're scoring a nature documentary.
|
||||
- ``saw`` + ``pluck`` = **funk stab.** Short, sharp, bright. The
|
||||
sound of Nile Rodgers' right hand.
|
||||
- ``hard_sync`` + ``pluck`` = **prophet lead.** Bright formant peak
|
||||
that cuts through any mix. The opening riff of every 80s synth solo.
|
||||
- ``wavefold`` + ``organ`` = **west coast bass.** Warm, harmonically
|
||||
rich sine-derivative that pairs beautifully with a lowpass after.
|
||||
- ``drift`` + ``pad`` = **analog pad.** A sawtooth that breathes and
|
||||
wobbles like a real VCO. Add chorus and reverb for Juno vibes.
|
||||
- ``mellotron_synth`` + ``organ`` = **prog strings.** Haunted tape
|
||||
machine. Add cathedral reverb and you're in 1972.
|
||||
|
||||
@@ -357,6 +357,32 @@ every tone knows its enharmonic spelling:
|
||||
>>> Tone.from_string("C4", system="western").enharmonic is None
|
||||
True
|
||||
|
||||
Accidental Properties
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Check whether a tone is natural, sharp, or flat:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> c = Tone.from_string("C4", system="western")
|
||||
>>> c.is_natural
|
||||
True
|
||||
>>> c.is_sharp
|
||||
False
|
||||
|
||||
>>> cs = Tone.from_string("C#4", system="western")
|
||||
>>> cs.is_sharp
|
||||
True
|
||||
>>> cs.is_natural
|
||||
False
|
||||
|
||||
>>> bb = Tone.from_string("Bb4", system="western")
|
||||
>>> bb.is_flat
|
||||
True
|
||||
|
||||
Useful for filtering — for example, finding all natural notes in a
|
||||
scale, or counting accidentals in a melody.
|
||||
|
||||
Extended Enharmonics
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ What's Inside
|
||||
guide/scales
|
||||
guide/chords
|
||||
guide/fretboard
|
||||
guide/nashville-blues-tabs
|
||||
guide/systems
|
||||
guide/sequencing
|
||||
guide/synths
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.40.4"
|
||||
version = "0.42.1"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.40.4"
|
||||
__version__ = "0.42.1"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
+434
-43
@@ -203,6 +203,168 @@ def pwm_fast_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
return pwm_wave(hz, peak, n_samples, lfo_rate=3.0)
|
||||
|
||||
|
||||
def hard_sync_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
|
||||
slave_ratio=1.5):
|
||||
"""Hard-sync oscillator — slave saw reset by master clock.
|
||||
|
||||
The quintessential analog lead sound. A "slave" oscillator runs at
|
||||
a different frequency but is forced to restart its cycle every time
|
||||
the "master" oscillator completes one. The abrupt restart creates
|
||||
bright, harmonically complex formant peaks that sweep as the slave
|
||||
ratio changes.
|
||||
|
||||
This is THE sound of the Prophet-5, Moog Prodigy, and every
|
||||
screaming analog lead since 1978.
|
||||
|
||||
Args:
|
||||
slave_ratio: Slave frequency as a multiple of master.
|
||||
1.0 = unison (plain saw). 1.5–3.0 = sweet spot for leads.
|
||||
Higher = more metallic, ring-mod-like.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
# Master phase ramps 0→1 at hz
|
||||
master_phase = (t * hz) % 1.0
|
||||
# Detect master zero-crossings (phase resets)
|
||||
resets = numpy.diff(master_phase, prepend=master_phase[0]) < -0.5
|
||||
# Slave phase: runs at slave_ratio * hz, but resets with master
|
||||
slave_phase = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
phase = 0.0
|
||||
slave_freq = hz * slave_ratio
|
||||
dt = 1.0 / SAMPLE_RATE
|
||||
for i in range(n_samples):
|
||||
if resets[i]:
|
||||
phase = 0.0
|
||||
slave_phase[i] = phase
|
||||
phase += slave_freq * dt
|
||||
phase %= 1.0
|
||||
# Slave is a sawtooth: 2*phase - 1
|
||||
wave = 2.0 * slave_phase - 1.0
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def ring_mod_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
|
||||
mod_ratio=1.5):
|
||||
"""Ring modulation — two oscillators multiplied together.
|
||||
|
||||
Multiplying two signals produces sum and difference frequencies,
|
||||
creating inharmonic, metallic, bell-like tones. Unlike FM, ring mod
|
||||
produces only sidebands — no carrier or modulator in the output.
|
||||
|
||||
Classic Dalek voice, Stockhausen elektronische Musik, and the
|
||||
metallic clang of every sci-fi soundtrack.
|
||||
|
||||
Args:
|
||||
mod_ratio: Modulator frequency as a multiple of carrier.
|
||||
Integer ratios (2, 3) = harmonic (bell-like).
|
||||
Non-integer (1.5, 2.1) = inharmonic (metallic, alien).
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
carrier = numpy.sin(2 * numpy.pi * hz * t)
|
||||
modulator = numpy.sin(2 * numpy.pi * hz * mod_ratio * t)
|
||||
wave = carrier * modulator
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def wavefold_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
|
||||
folds=3.0):
|
||||
"""Wavefolding — signal folded back on itself for complex harmonics.
|
||||
|
||||
The heart of west coast synthesis (Buchla, Make Noise, Verbos).
|
||||
A sine wave is amplified past ±1.0, then "folded" — the overflow
|
||||
bounces back instead of clipping. Each fold adds a new pair of
|
||||
harmonics. At low fold counts it's warm and round; crank it up
|
||||
and it gets buzzy, gnarly, and alive.
|
||||
|
||||
Sounds completely different from subtractive synthesis — instead
|
||||
of removing harmonics with a filter, you're *generating* them
|
||||
by shaping the wave. Pairs beautifully with a lowpass filter.
|
||||
|
||||
Args:
|
||||
folds: Drive amount. 1.0 = clean sine. 2–4 = sweet spot.
|
||||
6+ = harsh, buzzy territory.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
wave = numpy.sin(2 * numpy.pi * hz * t) * folds
|
||||
# Triangle-fold: repeatedly reflect at ±1
|
||||
# Uses the mathematical identity for folding
|
||||
wave = 4.0 * numpy.abs((wave / 4.0 + 0.25) % 1.0 - 0.5) - 1.0
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def drift_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
|
||||
shape="saw", drift_amount=0.15):
|
||||
"""Analog VCO with pitch drift, instability, and soft noise floor.
|
||||
|
||||
Real analog oscillators are never perfectly stable. Capacitor
|
||||
charging, thermal variations, and component tolerances make the
|
||||
pitch wander slightly. This is what makes a Minimoog sound "fat"
|
||||
and a VST sound "thin" — the constant micro-motion of imperfect
|
||||
hardware.
|
||||
|
||||
Models:
|
||||
- Slow pitch drift (< 1 Hz wander, like warming up)
|
||||
- Fast jitter (subtle per-cycle randomness)
|
||||
- Soft analog noise floor (faint hiss blended in)
|
||||
- Slightly rounded edges (no mathematically perfect transitions)
|
||||
|
||||
Args:
|
||||
shape: Base oscillator — "saw", "square", "triangle", or "pulse".
|
||||
drift_amount: How unstable the oscillator is.
|
||||
0.05 = studio-grade (Sequential, Oberheim).
|
||||
0.15 = classic vintage (Minimoog, ARP).
|
||||
0.3 = barely-holding-it-together (old SH-101).
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# Slow pitch drift — 2 LFOs at sub-Hz rates with random phase
|
||||
drift1 = drift_amount * 0.6 * numpy.sin(
|
||||
2 * numpy.pi * 0.07 * t + rng.uniform(0, 2 * numpy.pi))
|
||||
drift2 = drift_amount * 0.4 * numpy.sin(
|
||||
2 * numpy.pi * 0.23 * t + rng.uniform(0, 2 * numpy.pi))
|
||||
# Fast jitter — per-sample noise filtered to ~50 Hz bandwidth
|
||||
jitter_raw = rng.normal(0, drift_amount * 0.08, n_samples)
|
||||
# Simple one-pole lowpass for jitter smoothing
|
||||
alpha = 2 * numpy.pi * 50.0 / SAMPLE_RATE
|
||||
jitter = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
jitter[0] = jitter_raw[0]
|
||||
for i in range(1, n_samples):
|
||||
jitter[i] = jitter[i-1] + alpha * (jitter_raw[i] - jitter[i-1])
|
||||
|
||||
# Instantaneous frequency with drift (in cents, converted to ratio)
|
||||
cents_offset = drift1 + drift2 + jitter
|
||||
freq_ratio = 2.0 ** (cents_offset / 1200.0)
|
||||
|
||||
# Accumulate phase with varying frequency
|
||||
phase = numpy.cumsum(hz * freq_ratio / SAMPLE_RATE)
|
||||
phase %= 1.0
|
||||
|
||||
# Generate waveform from phase
|
||||
if shape == "square":
|
||||
wave = numpy.where(phase < 0.5, 1.0, -1.0)
|
||||
elif shape == "triangle":
|
||||
wave = 4.0 * numpy.abs(phase - 0.5) - 1.0
|
||||
elif shape == "pulse":
|
||||
wave = numpy.where(phase < 0.25, 1.0, -1.0)
|
||||
else: # saw
|
||||
wave = 2.0 * phase - 1.0
|
||||
|
||||
# Soft edges — gentle lowpass to round off transitions
|
||||
cutoff = min(16000, hz * 12)
|
||||
bl, al = scipy.signal.butter(1, cutoff, btype='low', fs=SAMPLE_RATE)
|
||||
wave = scipy.signal.lfilter(bl, al, wave)
|
||||
|
||||
# Subtle analog noise floor
|
||||
noise = rng.normal(0, 0.005, n_samples)
|
||||
wave = wave + noise
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def pluck_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Karplus-Strong plucked string synthesis.
|
||||
|
||||
@@ -534,6 +696,145 @@ def wurlitzer_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def mellotron_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
|
||||
tape="strings"):
|
||||
"""Mellotron — tape-replay keyboard from the 1960s.
|
||||
|
||||
Each key triggers a strip of magnetic tape with a pre-recorded
|
||||
instrument — the original "sampler." The mechanical transport gives
|
||||
it a lo-fi, haunted quality that no digital emulation fully captures:
|
||||
|
||||
- Tape flutter: pitch wobbles from uneven capstan speed
|
||||
- Limited bandwidth: 300 Hz–6 kHz, like a worn cassette
|
||||
- Tape saturation: soft compression, rounded transients
|
||||
- 8-second limit: tapes physically run out (we model the fadeout)
|
||||
- Head noise: faint hiss baked into the character
|
||||
|
||||
The Mellotron defined the sound of Strawberry Fields Forever,
|
||||
Stairway to Heaven, and every prog rock record from 1969–1977.
|
||||
|
||||
Args:
|
||||
tape: Which tape bank to simulate.
|
||||
"strings" — the iconic MkII string section
|
||||
"flute" — breathy, haunting solo flute
|
||||
"choir" — ghostly vocal pad
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# --- Tape flutter: slow wow + faster flutter ---
|
||||
wow = 0.12 * numpy.sin(2 * numpy.pi * 0.4 * t + rng.uniform(0, 6.28))
|
||||
flutter = 0.06 * numpy.sin(2 * numpy.pi * 6.3 * t + rng.uniform(0, 6.28))
|
||||
flutter += 0.03 * numpy.sin(2 * numpy.pi * 9.7 * t + rng.uniform(0, 6.28))
|
||||
# Cents of pitch deviation
|
||||
pitch_cents = wow + flutter
|
||||
freq_ratio = 2.0 ** (pitch_cents / 1200.0)
|
||||
|
||||
# Accumulate phase with flutter
|
||||
inst_freq = hz * freq_ratio
|
||||
phase = numpy.cumsum(inst_freq / SAMPLE_RATE)
|
||||
|
||||
# --- Generate the "tape" source ---
|
||||
nyquist = SAMPLE_RATE / 2.0
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
|
||||
if tape == "flute":
|
||||
# Breathy flute: fundamental + weak odd harmonics + breath noise
|
||||
wave += 0.7 * numpy.sin(2 * numpy.pi * phase)
|
||||
if hz * 3 < nyquist:
|
||||
wave += 0.12 * numpy.sin(2 * numpy.pi * 3 * phase + rng.uniform(0, 6.28))
|
||||
if hz * 5 < nyquist:
|
||||
wave += 0.04 * numpy.sin(2 * numpy.pi * 5 * phase + rng.uniform(0, 6.28))
|
||||
# Breath noise — bandpass filtered around fundamental
|
||||
breath = rng.normal(0, 0.25, n_samples)
|
||||
bw = max(100, hz * 0.3)
|
||||
lo = max(20, hz - bw)
|
||||
hi = min(nyquist * 0.95, hz + bw)
|
||||
bb, ab = scipy.signal.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
breath = scipy.signal.lfilter(bb, ab, breath)
|
||||
wave += breath
|
||||
elif tape == "choir":
|
||||
# Ghostly vocal pad: formant-shaped harmonics with slow drift
|
||||
formants = [
|
||||
(800, 100), # first formant ~'ah'
|
||||
(1200, 120), # second formant
|
||||
(2500, 200), # third formant
|
||||
]
|
||||
n_harmonics = min(20, int(nyquist / hz))
|
||||
for n in range(1, n_harmonics + 1):
|
||||
f_n = hz * n
|
||||
if f_n >= nyquist:
|
||||
break
|
||||
amp = 1.0 / n
|
||||
# Shape by formant peaks
|
||||
for f_center, f_bw in formants:
|
||||
amp *= 1.0 + 1.5 * numpy.exp(-((f_n - f_center) / f_bw) ** 2)
|
||||
p = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * n * phase + p)
|
||||
# Slow ensemble drift between "voices"
|
||||
drift = 0.005 * numpy.sin(2 * numpy.pi * 0.15 * t + rng.uniform(0, 6.28))
|
||||
wave2 = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
phase2 = numpy.cumsum(hz * (1.0 + drift) * freq_ratio / SAMPLE_RATE)
|
||||
for n in range(1, min(8, int(nyquist / hz)) + 1):
|
||||
f_n = hz * n
|
||||
if f_n >= nyquist:
|
||||
break
|
||||
amp = 0.6 / n
|
||||
for f_center, f_bw in formants:
|
||||
amp *= 1.0 + 1.5 * numpy.exp(-((f_n - f_center) / f_bw) ** 2)
|
||||
wave2 += amp * numpy.sin(2 * numpy.pi * n * phase2 + rng.uniform(0, 6.28))
|
||||
wave += wave2
|
||||
else:
|
||||
# Strings (default): layered ensemble with detuned unison
|
||||
n_harmonics = min(25, int(nyquist / hz))
|
||||
# Two "sections" slightly detuned for ensemble width
|
||||
for section_detune in [-1.5, 0, 1.5]:
|
||||
section_hz = hz * (2 ** (section_detune / 1200.0))
|
||||
section_phase = numpy.cumsum(section_hz * freq_ratio / SAMPLE_RATE)
|
||||
for n in range(1, n_harmonics + 1):
|
||||
f_n = section_hz * n
|
||||
if f_n >= nyquist:
|
||||
break
|
||||
# String-like: 1/n rolloff, even harmonics slightly weaker
|
||||
amp = 1.0 / n
|
||||
if n % 2 == 0:
|
||||
amp *= 0.8
|
||||
p = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * n * section_phase + p)
|
||||
|
||||
# Normalize before processing
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
|
||||
# --- Tape bandwidth limiting: 300 Hz – 6 kHz ---
|
||||
lo_cut = min(300, hz * 0.9) # don't cut fundamental
|
||||
hi_cut = min(6000, nyquist * 0.95)
|
||||
if lo_cut < hi_cut and lo_cut > 0:
|
||||
bb, ab = scipy.signal.butter(2, [lo_cut, hi_cut], btype='band', fs=SAMPLE_RATE)
|
||||
wave = scipy.signal.lfilter(bb, ab, wave)
|
||||
|
||||
# --- Tape saturation: soft compression ---
|
||||
wave = numpy.tanh(wave * 1.4) / 1.2
|
||||
|
||||
# --- Tape run-out: gentle fadeout after ~7 seconds ---
|
||||
if n_samples > int(SAMPLE_RATE * 7):
|
||||
fadeout_start = int(SAMPLE_RATE * 7)
|
||||
fadeout_len = n_samples - fadeout_start
|
||||
fade = numpy.linspace(1.0, 0.0, fadeout_len)
|
||||
wave[fadeout_start:] *= fade
|
||||
|
||||
# --- Head noise / tape hiss ---
|
||||
hiss = rng.normal(0, 0.008, n_samples)
|
||||
wave += hiss
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def vibraphone_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Vibraphone — struck aluminum bars with motor-driven tremolo.
|
||||
|
||||
@@ -1187,62 +1488,131 @@ def timpani_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
|
||||
|
||||
def saxophone_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Saxophone — single reed through a conical brass bore.
|
||||
"""Saxophone — single reed driving a conical brass bore.
|
||||
|
||||
The conical bore produces all harmonics (like oboe), but the
|
||||
brass body and larger mouthpiece give a warmer, fatter, more
|
||||
vocal quality. The reed adds a slight buzz. Saxophone is
|
||||
between clarinet (odd harmonics) and oboe (nasal even+odd) —
|
||||
it has everything, with a strong fundamental and rich mids.
|
||||
Models the key acoustic properties of a saxophone:
|
||||
1. Reed-bore interaction — nonlinear clipping creates the characteristic
|
||||
bright, edgy tone (not just additive sines)
|
||||
2. Conical bore formants — vocal-like resonances at ~500, ~1400, ~2300,
|
||||
~3200 Hz that give sax its singing quality
|
||||
3. Breath noise — turbulent airflow through the mouthpiece, strongest
|
||||
at attack and blending into sustained tone
|
||||
4. Sub-harmonic warmth — the conical bore's coupling creates warmth
|
||||
below the fundamental
|
||||
5. Vibrato — delayed onset, ~5 Hz, characteristic of jazz/classical sax
|
||||
"""
|
||||
import scipy.signal as _sig
|
||||
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# Vibrato — develops after ~250ms, wider than flute
|
||||
vib_onset = numpy.clip(t / 0.25, 0.0, 1.0)
|
||||
vib = hz * 0.0012 * vib_onset * numpy.sin(2 * numpy.pi * 5.2 * t)
|
||||
# --- Vibrato: delayed onset, subtle depth ---
|
||||
vib_onset = numpy.clip((t - 0.3) / 0.3, 0.0, 1.0)
|
||||
vib_rate = 5.0 + 0.15 * numpy.sin(2 * numpy.pi * 0.4 * t)
|
||||
vib = hz * 0.0006 * vib_onset * numpy.sin(2 * numpy.pi * vib_rate * t)
|
||||
|
||||
# --- Core tone: sawtooth-like waveform with reed clipping ---
|
||||
# Real sax reed creates a quasi-sawtooth pressure wave, not pure sines.
|
||||
# Build from harmonics with sax-specific spectral envelope, then clip.
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
n_harmonics = min(20, int((SAMPLE_RATE / 2) / hz))
|
||||
n_harmonics = min(25, int((SAMPLE_RATE / 2) / hz))
|
||||
|
||||
for n in range(1, n_harmonics + 1):
|
||||
f_n = hz * n
|
||||
if f_n >= SAMPLE_RATE / 2:
|
||||
break
|
||||
# Sax spectral shape: strong fundamental, broad mid peak (3-6),
|
||||
# slower rolloff than oboe (brass body carries harmonics further)
|
||||
# Saxophone spectral envelope from acoustic measurements:
|
||||
# Strong fundamental, nearly-as-strong 2nd and 3rd harmonics,
|
||||
# broad energy peak around harmonics 4-8 (the "body"), then
|
||||
# gradual rolloff — but slower than other woodwinds (brass bore
|
||||
# sustains upper partials).
|
||||
if n == 1:
|
||||
amp = 1.0
|
||||
elif n <= 3:
|
||||
amp = 0.6
|
||||
elif n <= 6:
|
||||
amp = 0.4 * numpy.exp(-0.1 * (n - 4) ** 2)
|
||||
elif n == 2:
|
||||
amp = 0.85
|
||||
elif n == 3:
|
||||
amp = 0.7
|
||||
elif n <= 8:
|
||||
# Broad mid peak — this is the sax "meat"
|
||||
amp = 0.55 * numpy.exp(-0.06 * (n - 5) ** 2)
|
||||
else:
|
||||
amp = 0.2 / n
|
||||
# Slower rolloff than oboe/clarinet
|
||||
amp = 0.35 / (n ** 0.7)
|
||||
|
||||
# Slight even/odd asymmetry — conical bore has all harmonics
|
||||
# but evens are ~10% weaker (midway between cylinder and cone)
|
||||
if n % 2 == 0:
|
||||
amp *= 0.9
|
||||
|
||||
phase = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * (f_n + vib * n) * t + phase)
|
||||
|
||||
# Reed buzz — more present than oboe but still warm
|
||||
reed = rng.normal(0, 0.07, n_samples)
|
||||
# Bandpass the reed noise around 1-3kHz (the "honk" range)
|
||||
import scipy.signal as _sig
|
||||
reed_lo = max(20, int(hz * 2))
|
||||
reed_hi = min(SAMPLE_RATE // 2 - 1, int(hz * 6))
|
||||
# --- Reed nonlinearity: soft clipping ---
|
||||
# The reed closes against the mouthpiece, creating asymmetric clipping
|
||||
# that adds brightness and "edge". This is what makes sax sound like
|
||||
# sax and not a flute.
|
||||
wave_max = numpy.abs(wave).max()
|
||||
if wave_max > 0:
|
||||
wave /= wave_max
|
||||
# Asymmetric soft clip: positive peaks clip harder (reed closure)
|
||||
wave = numpy.tanh(1.8 * wave) * 0.7 + numpy.tanh(2.5 * wave) * 0.3
|
||||
|
||||
# --- Formant resonances: conical bore creates vocal quality ---
|
||||
# These fixed resonances are what make sax sound "vocal" — they
|
||||
# emphasize certain frequency bands regardless of the note played.
|
||||
formant_freqs = [520, 1380, 2300, 3200]
|
||||
formant_bws = [120, 200, 280, 350]
|
||||
formant_gains = [0.25, 0.18, 0.12, 0.08]
|
||||
|
||||
formant_sum = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
for fc, bw, gain in zip(formant_freqs, formant_bws, formant_gains):
|
||||
lo = max(20, int(fc - bw))
|
||||
hi = min(SAMPLE_RATE // 2 - 1, int(fc + bw))
|
||||
if lo < hi:
|
||||
bf, af = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
formant_sum += _sig.lfilter(bf, af, wave) * gain
|
||||
wave = wave * 0.7 + formant_sum
|
||||
|
||||
# --- Breath noise: turbulent air through the mouthpiece ---
|
||||
# Strongest at the attack, then settles to a subtle constant hiss
|
||||
# that gives the tone "life" and prevents it from sounding synthetic.
|
||||
breath = rng.normal(0, 1.0, n_samples)
|
||||
# Shape breath noise into the sax's "hiss" band (2-6 kHz)
|
||||
breath_lo = max(20, 2000)
|
||||
breath_hi = min(SAMPLE_RATE // 2 - 1, 6000)
|
||||
if breath_lo < breath_hi:
|
||||
bb, ab = _sig.butter(2, [breath_lo, breath_hi], btype='band', fs=SAMPLE_RATE)
|
||||
breath = _sig.lfilter(bb, ab, breath)
|
||||
# Attack envelope for breath — strong at onset, then quiet
|
||||
breath_env = 0.15 * numpy.exp(-8.0 * t) + 0.03
|
||||
wave += breath * breath_env
|
||||
|
||||
# --- Reed buzz: low-frequency interaction noise ---
|
||||
# Different from breath — this is the "buzz" from reed vibration
|
||||
# against the mouthpiece, centered around the playing frequency.
|
||||
reed_noise = rng.normal(0, 1.0, n_samples)
|
||||
reed_lo = max(20, int(hz * 0.8))
|
||||
reed_hi = min(SAMPLE_RATE // 2 - 1, int(hz * 4))
|
||||
if reed_lo < reed_hi:
|
||||
br, ar = _sig.butter(2, [reed_lo, reed_hi], btype='band', fs=SAMPLE_RATE)
|
||||
reed = _sig.lfilter(br, ar, reed).astype(numpy.float64) * 2.0
|
||||
wave += reed
|
||||
reed_noise = _sig.lfilter(br, ar, reed_noise) * 0.06
|
||||
wave += reed_noise
|
||||
|
||||
# Brass body warmth — low-mid boost
|
||||
center = min(1500, hz * 4)
|
||||
bw = 500
|
||||
lo = max(20, int(center - bw))
|
||||
hi = min(SAMPLE_RATE // 2 - 1, int(center + bw))
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
body = _sig.lfilter(bp, ap, wave) * 0.2
|
||||
wave += body
|
||||
# --- Attack transient: key click + breath burst ---
|
||||
attack_len = min(int(SAMPLE_RATE * 0.015), n_samples)
|
||||
if attack_len > 0:
|
||||
click = rng.uniform(-1.0, 1.0, attack_len)
|
||||
click *= numpy.exp(-numpy.linspace(0, 8, attack_len))
|
||||
wave[:attack_len] += click * 0.12
|
||||
|
||||
# --- Sub-harmonic warmth ---
|
||||
# Conical bore coupling produces energy slightly below fundamental
|
||||
if hz > 80: # only if there's room
|
||||
sub = numpy.sin(2 * numpy.pi * (hz * 0.5) * t) * 0.04
|
||||
sub *= numpy.clip(t / 0.1, 0.0, 1.0) # fade in gently
|
||||
wave += sub
|
||||
|
||||
# --- Final shaping ---
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
@@ -2384,6 +2754,16 @@ class Synth(Enum):
|
||||
TINGSHA = "tingsha_synth"
|
||||
SINGING_BOWL_STRIKE = "singing_bowl_strike_synth"
|
||||
SINGING_BOWL_RING = "singing_bowl_ring_synth"
|
||||
RHODES = "rhodes_synth"
|
||||
WURLITZER = "wurlitzer_synth"
|
||||
VIBRAPHONE = "vibraphone_synth"
|
||||
PIPE_ORGAN = "pipe_organ_synth"
|
||||
CHOIR = "choir_synth"
|
||||
MELLOTRON = "mellotron_synth"
|
||||
HARD_SYNC = "hard_sync"
|
||||
RING_MOD = "ring_mod"
|
||||
WAVEFOLD = "wavefold"
|
||||
DRIFT = "drift"
|
||||
|
||||
def __call__(self, hz, **kwargs):
|
||||
"""Make Synth members callable — dispatches to the wave function."""
|
||||
@@ -2418,11 +2798,14 @@ _SYNTH_FUNCTIONS = {
|
||||
"tingsha_synth": tingsha_wave,
|
||||
"singing_bowl_strike_synth": singing_bowl_strike_wave,
|
||||
"singing_bowl_ring_synth": singing_bowl_ring_wave,
|
||||
"mellotron_synth": mellotron_wave,
|
||||
"hard_sync": hard_sync_wave, "ring_mod": ring_mod_wave,
|
||||
"wavefold": wavefold_wave, "drift": drift_wave,
|
||||
}
|
||||
|
||||
|
||||
def _render(tone_or_chord, temperament="equal", synth=Synth.SINE, t=1_000,
|
||||
envelope=Envelope.PIANO):
|
||||
envelope=Envelope.PIANO, **synth_kw):
|
||||
"""Render a tone or chord to a NumPy sample array.
|
||||
|
||||
Args:
|
||||
@@ -2434,6 +2817,9 @@ def _render(tone_or_chord, temperament="equal", synth=Synth.SINE, t=1_000,
|
||||
t: Duration in milliseconds.
|
||||
envelope: ADSR envelope preset. Use ``Envelope.NONE`` for raw
|
||||
output (old behavior).
|
||||
**synth_kw: Extra keyword arguments forwarded to the synth wave
|
||||
function (e.g. ``tape="flute"`` for Mellotron,
|
||||
``slave_ratio=2.0`` for Hard Sync).
|
||||
|
||||
Returns:
|
||||
A NumPy int16 array of audio samples.
|
||||
@@ -2441,10 +2827,10 @@ def _render(tone_or_chord, temperament="equal", synth=Synth.SINE, t=1_000,
|
||||
n_samples = int(SAMPLE_RATE * t / 1_000)
|
||||
|
||||
if isinstance(tone_or_chord, Tone):
|
||||
waves = [synth(tone_or_chord.pitch(temperament=temperament), n_samples=n_samples)]
|
||||
waves = [synth(tone_or_chord.pitch(temperament=temperament), n_samples=n_samples, **synth_kw)]
|
||||
else:
|
||||
waves = [
|
||||
synth(tone.pitch(temperament=temperament), n_samples=n_samples)
|
||||
synth(tone.pitch(temperament=temperament), n_samples=n_samples, **synth_kw)
|
||||
for tone in tone_or_chord.tones
|
||||
]
|
||||
|
||||
@@ -2459,7 +2845,7 @@ def _render(tone_or_chord, temperament="equal", synth=Synth.SINE, t=1_000,
|
||||
|
||||
|
||||
def play(tone_or_chord, temperament="equal", synth=Synth.SINE, t=1_000,
|
||||
envelope=Envelope.PIANO):
|
||||
envelope=Envelope.PIANO, **synth_kw):
|
||||
"""Play a tone or chord through the speakers.
|
||||
|
||||
Args:
|
||||
@@ -2471,19 +2857,22 @@ def play(tone_or_chord, temperament="equal", synth=Synth.SINE, t=1_000,
|
||||
t: Duration in milliseconds (default 1000).
|
||||
envelope: ADSR envelope preset (default ``Envelope.PIANO``).
|
||||
Use ``Envelope.NONE`` for raw waveform.
|
||||
**synth_kw: Extra keyword arguments forwarded to the synth wave
|
||||
function (e.g. ``tape="flute"`` for Mellotron).
|
||||
|
||||
Example::
|
||||
|
||||
>>> play(Tone.from_string("A4"), t=1_000)
|
||||
>>> play(Chord.from_name("Am7"), synth=Synth.TRIANGLE, t=2_000)
|
||||
>>> play(tone, envelope=Envelope.PAD, t=3_000)
|
||||
>>> play(tone, synth=Synth.MELLOTRON, tape="choir", t=2_000)
|
||||
"""
|
||||
_play_for(_render(tone_or_chord, temperament=temperament, synth=synth,
|
||||
t=t, envelope=envelope), ms=t)
|
||||
t=t, envelope=envelope, **synth_kw), ms=t)
|
||||
|
||||
|
||||
def save(tone_or_chord, path, temperament="equal", synth=Synth.SINE, t=1_000,
|
||||
envelope=Envelope.PIANO):
|
||||
envelope=Envelope.PIANO, **synth_kw):
|
||||
"""Render a tone or chord and save it as a WAV file.
|
||||
|
||||
Args:
|
||||
@@ -2493,6 +2882,8 @@ def save(tone_or_chord, path, temperament="equal", synth=Synth.SINE, t=1_000,
|
||||
synth: Waveform type.
|
||||
t: Duration in milliseconds (default 1000).
|
||||
envelope: ADSR envelope preset (default ``Envelope.PIANO``).
|
||||
**synth_kw: Extra keyword arguments forwarded to the synth wave
|
||||
function.
|
||||
|
||||
Example::
|
||||
|
||||
@@ -2502,7 +2893,7 @@ def save(tone_or_chord, path, temperament="equal", synth=Synth.SINE, t=1_000,
|
||||
import scipy.io.wavfile
|
||||
|
||||
samples = _render(tone_or_chord, temperament=temperament, synth=synth,
|
||||
t=t, envelope=envelope)
|
||||
t=t, envelope=envelope, **synth_kw)
|
||||
normalized = samples.astype(numpy.float32) / SAMPLE_PEAK
|
||||
# Convert to 16-bit PCM
|
||||
pcm = (normalized * 32767).astype(numpy.int16)
|
||||
@@ -5503,8 +5894,8 @@ def render_score(score):
|
||||
env_tuple = _resolve_envelope(part.envelope)
|
||||
# Use part swing if set, otherwise score swing
|
||||
effective_swing = part.swing if part.swing is not None else score.swing
|
||||
# Build synth-specific kwargs (e.g. FM ratio/index)
|
||||
synth_kwargs = {}
|
||||
# Build synth-specific kwargs (e.g. FM ratio/index, tape, folds)
|
||||
synth_kwargs = dict(getattr(part, 'synth_kw', None) or {})
|
||||
if part.synth in ("fm",):
|
||||
synth_kwargs["mod_ratio"] = part.fm_ratio
|
||||
synth_kwargs["mod_index"] = part.fm_index
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user