Compare commits

..

9 Commits

Author SHA1 Message Date
kennethreitz 92ade3ee3d v0.36.2: REPL updates, 862 tests, improved songs, Ctrl-C handling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:28:49 -04:00
kennethreitz 833867329e REPL: new commands, all instruments, updated autocomplete
New commands: strum, roll, bend, temperament, reference, instruments
Updated autocomplete: 41 synths, 50+ instruments, bowed/mallet
envelopes, all drum patterns (tabla, dhol, djembe, cajón, metal)
Part command supports instrument= keyword
Status shows temperament and reference pitch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:27:41 -04:00
kennethreitz 93b9fe9ced 25 new tests: all new synths, vocal, cajón, bends, rolls, int tones
862 tests total. Covers: 11 new synth waveforms, vocal synth with
lyrics, all instrument presets, cajón drums/patterns, pitch bend
rendering (3 types), roll velocity ramp, int tone names + wrapping,
B#/Cb octave fix, note choking, Score system/temperament/ref_pitch,
synth enum count (41).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:27:06 -04:00
kennethreitz 88a1171bbe Fix Theremin Noir: granular pad → strings pad (less noisy)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:20:01 -04:00
kennethreitz 3ca0842b7a Improve songs 1-16, 19: humanize, reverb, velocity dynamics
- humanize=0.2 added to all melodic parts (leads, basses, bells)
- Subtle reverb (0.1-0.2) on bass parts that had none
- Per-note velocity dynamics on all leads (was static)
- Blues lead changed from trumpet to saxophone (more fitting)
- Songs 17-18, 20-26 left untouched (already well-crafted)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:19:22 -04:00
kennethreitz 00de5eb354 Catch KeyboardInterrupt in all playback functions
play(), play_score(), _play_for() now catch Ctrl-C and stop
cleanly instead of crashing with a traceback. CLI demo also
wrapped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:18:43 -04:00
kennethreitz d2b0c6f329 v0.36.1: 7 new synths, 9 new demo moods
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:15:22 -04:00
kennethreitz 76612682f1 9 new demo moods: theremin noir, caribbean, accordion waltz, kalimba
dreams, outback drone, highland, nashville tears, tabla fusion

All new synths represented in pytheory demo random rotation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:13:11 -04:00
kennethreitz ce480858e9 7 new synths: pedal steel, theremin, kalimba, steel drum, accordion, didgeridoo, bagpipe
- Pedal steel: singing harmonics, slow vibrato, spring reverb
- Theremin: pure sine with hand wobble, legato+glide preset
- Kalimba: inharmonic metal tine modes, wooden body, bell-like
- Steel drum: hammered metal partials, bright Caribbean ring
- Accordion: musette-tuned doubled reeds, bellows pressure swell
- Didgeridoo: deep cylindrical drone, shifting formant overtones
- Bagpipe: bright chanter reed, constant bag pressure
- 41 synth waveforms total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:11:32 -04:00
11 changed files with 972 additions and 201 deletions
+11
View File
@@ -2,6 +2,17 @@
All notable changes to PyTheory are documented here.
## 0.36.1
- **7 new instrument synths:** pedal steel guitar, theremin, kalimba/thumb
piano, steel drum/pan, accordion (musette reeds), didgeridoo (drone +
shifting formants), bagpipes (chanter reed)
- **9 new demo moods** in ``pytheory demo``: Theremin Noir, Caribbean,
Accordion Waltz, Kalimba Dreams, Outback Drone, Highland, Nashville
Tears, Tabla Fusion
- Improved existing songs with dedicated instrument synths
- 41 synth waveforms, 26+ songs, 21 demo moods
## 0.36.0
- **Banjo synth** — steel strings on drum-head body, nasal twang,
+1 -1
View File
@@ -77,7 +77,7 @@ What's Inside
numbers), scale recommendation, modulation, voice leading
- **Sequencing** — Score, Parts, arpeggiator, legato/glide, velocity,
swing, humanize, tempo changes, song sections with repeat
- **Synthesis**34 waveforms (including Karplus-Strong pluck, Hammond organ,
- **Synthesis** — 41 waveforms (including Karplus-Strong pluck, Hammond organ,
bowed string, and 14 dedicated instrument synths), 10 envelopes, 40+
instrument presets, configurable FM, sub-oscillator, noise layer, filter
envelope, velocity-to-brightness, analog oscillator drift, detune, stereo
+215 -184
View File
@@ -52,24 +52,26 @@ def bossa_nova_girl():
lead = score.part("lead", instrument="flute",
volume=0.45, pan=0.3,
delay=0.25, delay_time=0.32, delay_feedback=0.35,
reverb=0.2, reverb_type="plate")
reverb=0.2, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", instrument="upright_bass",
volume=0.45, pan=0.0, lowpass=600)
volume=0.45, pan=0.0, lowpass=600,
humanize=0.2, reverb=0.2)
for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
for n, d in [
("E5",.67),("D5",.33),("C5",.67),("B4",.33),("A4",1),("C5",.67),("E5",.33),
("D5",.67),("C5",.33),("A4",1),(None,1),
("F5",.67),("E5",.33),("D5",.67),("C5",.33),("D5",1),("F5",.67),("A5",.33),
("G5",.67),("F5",.33),("D5",1),(None,1),
("G#5",.67),("F5",.33),("E5",.67),("D5",.33),("E5",1),(None,.5),("B4",.5),
("D5",.67),("E5",.33),("G#4",1),(None,1),
("A4",1),("C5",.67),("E5",.33),("A5",1.5),(None,.5),
("G5",.67),("E5",.33),("C5",.67),("A4",.33),("A4",2),
for n, d, v in [
("E5",.67,85),("D5",.33,75),("C5",.67,80),("B4",.33,70),("A4",1,85),("C5",.67,78),("E5",.33,82),
("D5",.67,78),("C5",.33,72),("A4",1,80),(None,1,0),
("F5",.67,88),("E5",.33,78),("D5",.67,82),("C5",.33,72),("D5",1,80),("F5",.67,85),("A5",.33,90),
("G5",.67,82),("F5",.33,75),("D5",1,78),(None,1,0),
("G#5",.67,92),("F5",.33,78),("E5",.67,85),("D5",.33,75),("E5",1,82),(None,.5,0),("B4",.5,72),
("D5",.67,78),("E5",.33,82),("G#4",1,75),(None,1,0),
("A4",1,80),("C5",.67,78),("E5",.33,85),("A5",1.5,95),(None,.5,0),
("G5",.67,82),("E5",.33,78),("C5",.67,75),("A4",.33,70),("A4",2,85),
]:
lead.rest(d) if n is None else lead.add(n, d)
lead.rest(d) if n is None else lead.add(n, d, velocity=v)
for n in ["A2","E2","A2","C3","D2","A2","D2","F2",
"E2","B2","E2","G#2","A2","E2","A2","C3",
@@ -95,32 +97,34 @@ def bebop_in_bb():
volume=0.4, pan=0.25,
lowpass=4000, lowpass_q=1.1,
delay=0.15, delay_time=0.19, delay_feedback=0.25,
reverb=0.15, reverb_type="plate")
reverb=0.15, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", instrument="upright_bass",
volume=0.4, pan=0.0, lowpass=500)
volume=0.4, pan=0.0, lowpass=500,
humanize=0.2, reverb=0.2)
for sym in ["Bb", "Gm", "Cm", "F7"] * 2:
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
for n, d in [
("Bb4",.67),("D5",.33),("F5",.67),("D5",.33),
("Bb4",.67),("C5",.33),("D5",.67),("F5",.33),
("G5",.67),("F5",.33),("D5",.67),("Bb4",.33),
("A4",.67),("Bb4",.33),("D5",.67),("G4",.33),
("C5",.67),("Eb5",.33),("G5",.67),("Eb5",.33),
("C5",.67),("D5",.33),("Eb5",.67),("F5",.33),
("A5",.67),("G5",.33),("F5",.67),("Eb5",.33),
("D5",.67),("C5",.33),("A4",.5),(None,.5),
("Bb4",1),("D5",.67),("F5",.33),
("G5",.67),("F5",.33),("D5",.67),("Bb4",.33),
("Bb5",.67),("A5",.33),("G5",.67),("F5",.33),
("Eb5",.67),("D5",.33),("Bb4",.67),("G4",.33),
("C5",.5),(None,.5),("Eb5",.67),("G5",.33),
("F5",.67),("Eb5",.33),("D5",.67),("C5",.33),
("A4",.67),("C5",.33),("Eb5",.67),("F5",.33),
("G5",.67),("A5",.33),("Bb5",1),
for n, d, v in [
("Bb4",.67,82),("D5",.33,75),("F5",.67,88),("D5",.33,78),
("Bb4",.67,80),("C5",.33,72),("D5",.67,82),("F5",.33,85),
("G5",.67,90),("F5",.33,78),("D5",.67,82),("Bb4",.33,72),
("A4",.67,78),("Bb4",.33,72),("D5",.67,85),("G4",.33,70),
("C5",.67,82),("Eb5",.33,78),("G5",.67,92),("Eb5",.33,80),
("C5",.67,78),("D5",.33,75),("Eb5",.67,82),("F5",.33,85),
("A5",.67,95),("G5",.33,82),("F5",.67,85),("Eb5",.33,78),
("D5",.67,80),("C5",.33,72),("A4",.5,75),(None,.5,0),
("Bb4",1,85),("D5",.67,80),("F5",.33,88),
("G5",.67,90),("F5",.33,82),("D5",.67,78),("Bb4",.33,72),
("Bb5",.67,95),("A5",.33,85),("G5",.67,88),("F5",.33,80),
("Eb5",.67,82),("D5",.33,75),("Bb4",.67,78),("G4",.33,70),
("C5",.5,78),(None,.5,0),("Eb5",.67,82),("G5",.33,88),
("F5",.67,85),("Eb5",.33,78),("D5",.67,82),("C5",.33,75),
("A4",.67,78),("C5",.33,80),("Eb5",.67,85),("F5",.33,88),
("G5",.67,92),("A5",.33,88),("Bb5",1,95),
]:
lead.rest(d) if n is None else lead.add(n, d)
lead.rest(d) if n is None else lead.add(n, d, velocity=v)
for n in ["Bb2","D3","F3","A3","G3","F3","D3","Bb2",
"C3","Eb3","G3","Bb3","F3","A3","C4","Eb3",
@@ -146,29 +150,31 @@ def salsa_descarga():
lead = score.part("lead", instrument="trumpet",
volume=0.4, pan=0.3,
delay=0.2, delay_time=0.167, delay_feedback=0.3,
reverb=0.15, reverb_type="plate")
reverb=0.15, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", instrument="synth_bass",
volume=0.45, pan=0.0, lowpass=500, lowpass_q=1.3)
volume=0.45, pan=0.0, lowpass=500, lowpass_q=1.3,
humanize=0.2, reverb=0.2)
for sym in ["Em7b5", "A7", "Dm7", "Bbmaj7"] * 2:
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
for n, d in [
("E5",.67),("G5",.33),("Bb5",.67),("A5",.33),
("G5",.67),("F5",.33),("E5",.67),("D5",.33),
("C#5",.67),("D5",.33),("E5",.67),("G5",.33),
("F5",.67),("E5",.33),("C#5",.5),(None,.5),
("D5",.5),(None,.17),("F5",.67),("A5",.33),
("G5",.67),("F5",.33),("E5",.67),("D5",.33),
("Bb4",1),("D5",.67),("F5",.33),("A5",1),(None,1),
("E5",.5),("F5",.5),("G5",.67),("A5",.33),
("Bb5",.67),("A5",.33),("G5",.67),("E5",.33),
("C#5",.67),("E5",.33),("A5",.67),("G5",.33),
("F5",.67),("E5",.33),("C#5",.67),("A4",.33),
("D5",1),("F5",.67),("A5",.33),("G5",.67),("F5",.33),("D5",1),(None,1),
("Bb4",.67),("D5",.33),("F5",.67),("Bb5",.33),("A5",1.5),(None,.5),
for n, d, v in [
("E5",.67,85),("G5",.33,78),("Bb5",.67,92),("A5",.33,82),
("G5",.67,85),("F5",.33,78),("E5",.67,82),("D5",.33,75),
("C#5",.67,80),("D5",.33,75),("E5",.67,85),("G5",.33,80),
("F5",.67,82),("E5",.33,78),("C#5",.5,75),(None,.5,0),
("D5",.5,78),(None,.17,0),("F5",.67,85),("A5",.33,90),
("G5",.67,88),("F5",.33,80),("E5",.67,82),("D5",.33,75),
("Bb4",1,78),("D5",.67,82),("F5",.33,88),("A5",1,95),(None,1,0),
("E5",.5,80),("F5",.5,82),("G5",.67,88),("A5",.33,85),
("Bb5",.67,95),("A5",.33,85),("G5",.67,88),("E5",.33,78),
("C#5",.67,80),("E5",.33,82),("A5",.67,92),("G5",.33,85),
("F5",.67,82),("E5",.33,78),("C#5",.67,75),("A4",.33,70),
("D5",1,82),("F5",.67,85),("A5",.33,92),("G5",.67,88),("F5",.33,80),("D5",1,78),(None,1,0),
("Bb4",.67,75),("D5",.33,80),("F5",.67,88),("Bb5",.33,95),("A5",1.5,90),(None,.5,0),
]:
lead.rest(d) if n is None else lead.add(n, d)
lead.rest(d) if n is None else lead.add(n, d, velocity=v)
for n in ["E2","E3","A2","A3","D2","D3","Bb2","Bb3"] * 4:
bass.add(n, Duration.QUARTER)
@@ -192,23 +198,25 @@ def afrobeat_groove():
volume=0.4, pan=0.3,
lowpass=3000, lowpass_q=1.0,
delay=0.2, delay_time=0.26, delay_feedback=0.3,
reverb=0.15, reverb_type="plate")
reverb=0.15, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", instrument="bass_guitar",
volume=0.5, pan=0.0, lowpass=500)
volume=0.5, pan=0.0, lowpass=500,
humanize=0.2, reverb=0.2)
for sym in ["Em", "Am", "D", "C"] * 2:
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
riff = [("E5",.5),("G5",.5),("A5",.5),("G5",.5),
("E5",.5),("D5",.5),("E5",1),
("E5",.5),("G5",.5),("A5",.5),("B5",.5),
("A5",.5),("G5",.5),("E5",1),
(None,.5),("A5",.5),("G5",.5),("E5",.5),
("D5",1),("E5",.5),("G5",.5),
("A5",.5),("B5",.5),("A5",.5),("G5",.5),
("E5",1.5),(None,.5)]
for n, d in riff * 2:
lead.rest(d) if n is None else lead.add(n, d)
riff = [("E5",.5,82),("G5",.5,78),("A5",.5,88),("G5",.5,80),
("E5",.5,78),("D5",.5,72),("E5",1,85),
("E5",.5,80),("G5",.5,78),("A5",.5,88),("B5",.5,92),
("A5",.5,85),("G5",.5,78),("E5",1,82),
(None,.5,0),("A5",.5,85),("G5",.5,80),("E5",.5,75),
("D5",1,78),("E5",.5,80),("G5",.5,82),
("A5",.5,88),("B5",.5,92),("A5",.5,85),("G5",.5,80),
("E5",1.5,82),(None,.5,0)]
for n, d, v in riff * 2:
lead.rest(d) if n is None else lead.add(n, d, velocity=v)
for n in ["E2","E2","G2","A2","A2","G2","E2","D2",
"D2","D2","F#2","A2","C3","C3","B2","G2"] * 2:
@@ -234,24 +242,26 @@ def reggae_one_drop():
lead = score.part("lead", instrument="flute",
volume=0.4, pan=0.3,
delay=0.35, delay_time=0.5625, delay_feedback=0.45,
reverb=0.3, reverb_type="cathedral")
reverb=0.3, reverb_type="cathedral",
humanize=0.2)
bass = score.part("bass", instrument="bass_guitar",
volume=0.55, pan=0.0, lowpass=400, lowpass_q=1.3)
volume=0.55, pan=0.0, lowpass=400, lowpass_q=1.3,
humanize=0.2, reverb=0.2)
for sym in ["G", "C", "D", "C"] * 2:
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
for n, d in [
("G5",1.5),(None,.5),("B5",1),("A5",1),
("G5",2),("E5",1),("D5",1),
("C5",1.5),(None,.5),("E5",1),("G5",1),
("A5",1.5),(None,.5),("G5",2),
("D5",1.5),(None,.5),("E5",1),("G5",1),
("A5",2),("B5",1),("A5",1),
("G5",1.5),(None,.5),("E5",1),("D5",1),
("G4",3),(None,1),
for n, d, v in [
("G5",1.5,85),(None,.5,0),("B5",1,90),("A5",1,82),
("G5",2,80),("E5",1,75),("D5",1,72),
("C5",1.5,78),(None,.5,0),("E5",1,80),("G5",1,85),
("A5",1.5,88),(None,.5,0),("G5",2,82),
("D5",1.5,78),(None,.5,0),("E5",1,80),("G5",1,85),
("A5",2,90),("B5",1,92),("A5",1,85),
("G5",1.5,82),(None,.5,0),("E5",1,78),("D5",1,72),
("G4",3,75),(None,1,0),
]:
lead.rest(d) if n is None else lead.add(n, d)
lead.rest(d) if n is None else lead.add(n, d, velocity=v)
for n in ["G2","G2","B2","D3","C3","C3","E3","G3",
"D3","D3","F#3","A3","C3","C3","E3","G3",
@@ -278,30 +288,32 @@ def funk_workout():
volume=0.4, pan=0.35,
lowpass=3500, lowpass_q=1.5,
delay=0.15, delay_time=0.15, delay_feedback=0.25,
reverb=0.1, reverb_type="plate")
reverb=0.1, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", instrument="synth_bass",
volume=0.5, pan=0.0, lowpass=600, lowpass_q=1.2)
volume=0.5, pan=0.0, lowpass=600, lowpass_q=1.2,
humanize=0.2, reverb=0.15)
for sym in ["Em", "Am", "D", "B7"] * 2:
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
for n, d in [
("E5",.25),("E5",.25),(None,.25),("G5",.25),
(None,.25),("A5",.25),("G5",.25),("E5",.25),
("D5",.5),("E5",.5),(None,.5),("B4",.5),
("E5",.25),("E5",.25),(None,.25),("G5",.25),
(None,.25),("A5",.25),("B5",.25),("A5",.25),
("G5",.5),("E5",.5),(None,1),
("A4",.25),("C5",.25),("E5",.25),("A5",.25),
("G5",.5),("E5",.5),(None,.5),("C5",.5),
("A4",.5),("C5",.5),("D5",.5),("E5",.5),
("E5",1),(None,1),
("D5",.25),("F#5",.25),("A5",.25),("D5",.25),
("F#5",.5),("D5",.5),("A4",.5),("D5",.5),
("D#5",.5),("F#5",.5),("B4",.5),("D#5",.5),
("F#5",1),(None,1),
for n, d, v in [
("E5",.25,90),("E5",.25,78),(None,.25,0),("G5",.25,85),
(None,.25,0),("A5",.25,88),("G5",.25,82),("E5",.25,78),
("D5",.5,80),("E5",.5,85),(None,.5,0),("B4",.5,72),
("E5",.25,88),("E5",.25,78),(None,.25,0),("G5",.25,85),
(None,.25,0),("A5",.25,90),("B5",.25,92),("A5",.25,85),
("G5",.5,82),("E5",.5,78),(None,1,0),
("A4",.25,80),("C5",.25,82),("E5",.25,88),("A5",.25,92),
("G5",.5,85),("E5",.5,78),(None,.5,0),("C5",.5,75),
("A4",.5,78),("C5",.5,80),("D5",.5,82),("E5",.5,85),
("E5",1,82),(None,1,0),
("D5",.25,82),("F#5",.25,85),("A5",.25,90),("D5",.25,78),
("F#5",.5,85),("D5",.5,78),("A4",.5,75),("D5",.5,80),
("D#5",.5,82),("F#5",.5,88),("B4",.5,78),("D#5",.5,82),
("F#5",1,85),(None,1,0),
]:
lead.rest(d) if n is None else lead.add(n, d)
lead.rest(d) if n is None else lead.add(n, d, velocity=v)
for n in ["E2","E2","G2","E2","A2","A2","C3","A2",
"D2","D2","F#2","D2","B1","B1","D#2","F#2"] * 2:
@@ -313,7 +325,7 @@ def funk_workout():
def blues_shuffle():
"""12/8 blues in A — slow shuffle with wailing lead."""
print(" 12/8 Blues Shuffle in A")
print(" 12/8 blues drums | saw lead + reverb + delay | sine bass + LP 500Hz")
print(" 12/8 blues drums | saxophone lead + reverb + delay | upright bass + LP 500Hz")
score = Score("12/8", bpm=70)
score.drums("12/8 blues", repeats=6)
@@ -321,32 +333,34 @@ def blues_shuffle():
chords = score.part("chords", instrument="electric_piano",
volume=0.3, pan=-0.3,
reverb=0.3, reverb_decay=1.5, reverb_type="plate")
lead = score.part("lead", instrument="trumpet",
lead = score.part("lead", instrument="saxophone",
volume=0.45, pan=0.25,
reverb=0.3, reverb_decay=1.2, reverb_type="plate",
delay=0.2, delay_time=0.43, delay_feedback=0.3,
lowpass=3500)
lowpass=3500,
humanize=0.2)
bass = score.part("bass", instrument="upright_bass",
volume=0.5, pan=0.0, lowpass=500)
volume=0.5, pan=0.0, lowpass=500,
humanize=0.2, reverb=0.2)
for sym in ["A", "A", "D", "D", "E7", "A"]:
chords.add(Chord.from_symbol(sym), Duration.DOTTED_HALF)
chords.add(Chord.from_symbol(sym), Duration.DOTTED_HALF)
for n, d in [
("A4",1),("C5",.67),("A4",.33),("E4",1),
(None,.5),("A4",.5),("C5",.67),("D5",.33),
("E5",1.5),("D5",.5),("C5",.5),("A4",.5),
("E4",1.5),(None,.5),("A4",1),
("D5",1),("F5",.67),("D5",.33),("A4",1),
(None,1),("D5",.67),("F5",.33),("A5",1),
("G5",.67),("F5",.33),("D5",1),(None,1),
("E5",.67),("G#4",.33),("B4",.67),("E5",.33),
("D5",1),("A4",1),(None,1),
("A4",.67),("C5",.33),("E5",.67),("A5",.33),
("A5",2),(None,1),
for n, d, v in [
("A4",1,80),("C5",.67,85),("A4",.33,75),("E4",1,78),
(None,.5,0),("A4",.5,80),("C5",.67,85),("D5",.33,82),
("E5",1.5,92),("D5",.5,80),("C5",.5,78),("A4",.5,75),
("E4",1.5,72),(None,.5,0),("A4",1,80),
("D5",1,85),("F5",.67,90),("D5",.33,78),("A4",1,80),
(None,1,0),("D5",.67,82),("F5",.33,88),("A5",1,95),
("G5",.67,88),("F5",.33,80),("D5",1,78),(None,1,0),
("E5",.67,85),("G#4",.33,72),("B4",.67,80),("E5",.33,85),
("D5",1,82),("A4",1,78),(None,1,0),
("A4",.67,78),("C5",.33,82),("E5",.67,90),("A5",.33,95),
("A5",2,92),(None,1,0),
]:
lead.rest(d) if n is None else lead.add(n, d)
lead.rest(d) if n is None else lead.add(n, d, velocity=v)
for n in ["A1","A1","E2","A2","A1","A1","E2","A2","A1","A1","E2","A2",
"D2","D2","A2","D2","D2","D2","A2","D2",
@@ -372,25 +386,27 @@ def samba_de_janeiro():
lead = score.part("lead", instrument="flute",
volume=0.45, pan=0.3,
delay=0.2, delay_time=0.176, delay_feedback=0.3,
reverb=0.15, reverb_type="plate")
reverb=0.15, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", instrument="bass_guitar",
volume=0.45, pan=0.0, lowpass=500)
volume=0.45, pan=0.0, lowpass=500,
humanize=0.2, reverb=0.2)
for sym in ["G", "Em", "Am", "D7"] * 2:
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
for n, d in [
("B5",.33),("A5",.33),("G5",.34),("F#5",.5),("E5",.5),
("D5",.5),("G5",.5),("B5",.5),("A5",.5),
("G5",.67),("E5",.33),("D5",.67),("B4",.33),("G4",1),(None,1),
("E5",.5),("G5",.5),("B5",.5),("A5",.5),
("G5",.67),("F#5",.33),("E5",.67),("D5",.33),("E5",1),(None,1),
("A5",.5),("C6",.5),("B5",.5),("A5",.5),
("G5",.67),("E5",.33),("C5",.67),("A4",.33),("A4",1),(None,1),
("D5",.5),("F#5",.5),("A5",.5),("C5",.5),
("B4",.67),("A4",.33),("G4",.67),("F#4",.33),("G4",2),(None,2),
for n, d, v in [
("B5",.33,88),("A5",.33,82),("G5",.34,78),("F#5",.5,82),("E5",.5,75),
("D5",.5,72),("G5",.5,80),("B5",.5,88),("A5",.5,82),
("G5",.67,85),("E5",.33,78),("D5",.67,75),("B4",.33,70),("G4",1,72),(None,1,0),
("E5",.5,80),("G5",.5,85),("B5",.5,90),("A5",.5,82),
("G5",.67,85),("F#5",.33,80),("E5",.67,78),("D5",.33,72),("E5",1,80),(None,1,0),
("A5",.5,85),("C6",.5,92),("B5",.5,88),("A5",.5,82),
("G5",.67,85),("E5",.33,78),("C5",.67,75),("A4",.33,70),("A4",1,72),(None,1,0),
("D5",.5,78),("F#5",.5,85),("A5",.5,90),("C5",.5,75),
("B4",.67,78),("A4",.33,72),("G4",.67,70),("F#4",.33,68),("G4",2,75),(None,2,0),
]:
lead.rest(d) if n is None else lead.add(n, d)
lead.rest(d) if n is None else lead.add(n, d, velocity=v)
for n in ["G2","B2","D3","G2","E2","G2","B2","E2",
"A2","C3","E3","A2","D2","F#2","A2","D3"] * 2:
@@ -413,26 +429,28 @@ def jazz_waltz():
lead = score.part("lead", instrument="flute",
volume=0.4, pan=0.25,
reverb=0.3, reverb_decay=1.5, reverb_type="plate",
delay=0.2, delay_time=0.4, delay_feedback=0.3)
delay=0.2, delay_time=0.4, delay_feedback=0.3,
humanize=0.2)
bass = score.part("bass", instrument="upright_bass",
volume=0.4, pan=0.0, lowpass=500)
volume=0.4, pan=0.0, lowpass=500,
humanize=0.2, reverb=0.2)
for _ in range(2):
for sym in ["Fmaj7", "Gm", "C7", "Fmaj7"]:
for _ in range(4):
rhodes.add(Chord.from_symbol(sym), Duration.DOTTED_HALF)
for n, d in [
("A5",1.5),("G5",.5),("F5",1),("E5",1),("C5",1),("F5",1),
("A5",2),(None,1),("G5",2),(None,1),
("Bb5",1),("A5",.5),("G5",.5),("F5",1),("D5",1),("G5",1),
("Bb5",2),(None,1),("A5",1.5),("G5",.5),("F5",1),
("E5",1),("G5",1),("Bb5",1),("A5",1.5),("G5",.5),("E5",1),
("C5",2),(None,1),("E5",1),("G5",1),("C5",1),
("F5",2),("A5",1),("C6",2),(None,1),
("A5",1),("F5",1),("C5",1),("F5",3),
for n, d, v in [
("A5",1.5,85),("G5",.5,78),("F5",1,80),("E5",1,75),("C5",1,72),("F5",1,80),
("A5",2,88),(None,1,0),("G5",2,82),(None,1,0),
("Bb5",1,90),("A5",.5,82),("G5",.5,78),("F5",1,80),("D5",1,75),("G5",1,82),
("Bb5",2,90),(None,1,0),("A5",1.5,85),("G5",.5,78),("F5",1,80),
("E5",1,78),("G5",1,82),("Bb5",1,88),("A5",1.5,85),("G5",.5,78),("E5",1,75),
("C5",2,72),(None,1,0),("E5",1,78),("G5",1,82),("C5",1,72),
("F5",2,82),("A5",1,88),("C6",2,92),(None,1,0),
("A5",1,85),("F5",1,80),("C5",1,72),("F5",3,82),
]:
lead.rest(d) if n is None else lead.add(n, d)
lead.rest(d) if n is None else lead.add(n, d, velocity=v)
for n in ["F2","A2","C3","G2","Bb2","D3","C2","E2","G2","F2","A2","C3"] * 4:
bass.add(n, Duration.QUARTER)
@@ -461,7 +479,8 @@ def house_anthem():
reverb=0.15, reverb_type="plate")
bass = score.part("bass", instrument="808_bass",
volume=0.55, pan=0.0,
sidechain=0.5)
sidechain=0.5,
reverb=0.1)
for sym in ["Cm", "Ab", "Bb", "Cm"] * 2:
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -518,9 +537,11 @@ def dub_kingston():
lead = score.part("lead", instrument="flute",
volume=0.4, pan=0.3,
delay=0.45, delay_time=0.625, delay_feedback=0.5,
reverb=0.35, reverb_decay=2.0, reverb_type="cathedral")
reverb=0.35, reverb_decay=2.0, reverb_type="cathedral",
humanize=0.2)
bass = score.part("bass", instrument="bass_guitar",
volume=0.6, pan=0.0, lowpass=400, lowpass_q=1.5)
volume=0.6, pan=0.0, lowpass=400, lowpass_q=1.5,
humanize=0.2)
siren = score.part("siren", synth="pwm_slow", envelope="pad",
volume=0.15, pan=0.5,
reverb=0.7, reverb_decay=3.0, reverb_type="cathedral",
@@ -529,13 +550,13 @@ def dub_kingston():
for sym in ["Am", "Am", "Dm", "Dm", "Am", "Am", "Em", "Am"]:
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
for n, d in [
("A4", 2), (None, 2), ("C5", 1.5), (None, 2.5),
("D5", 1), ("C5", 1), ("A4", 2), (None, 4),
("E5", 2), (None, 2), ("D5", 1.5), ("C5", 1.5), (None, 3),
("A4", 1), ("G4", 1), ("A4", 3), (None, 3),
for n, d, v in [
("A4", 2, 78), (None, 2, 0), ("C5", 1.5, 82), (None, 2.5, 0),
("D5", 1, 85), ("C5", 1, 78), ("A4", 2, 75), (None, 4, 0),
("E5", 2, 88), (None, 2, 0), ("D5", 1.5, 82), ("C5", 1.5, 78), (None, 3, 0),
("A4", 1, 75), ("G4", 1, 70), ("A4", 3, 78), (None, 3, 0),
]:
lead.rest(d) if n is None else lead.add(n, d)
lead.rest(d) if n is None else lead.add(n, d, velocity=v)
for n in ["A1","A1","A1","A1","D1","D1","D1","D1",
"A1","A1","A1","A1","E1","E1","A1","A1"]:
@@ -573,7 +594,8 @@ def techno_minimal():
humanize=0.2)
bass = score.part("bass", instrument="808_bass",
volume=0.55, pan=0.0,
sidechain=0.5)
sidechain=0.5,
reverb=0.1)
for sym in ["Fm", "Db", "Eb", "Fm"] * 2:
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -605,31 +627,33 @@ def gospel_shuffle():
lead = score.part("lead", instrument="flute",
volume=0.4, pan=0.3,
delay=0.2, delay_time=0.278, delay_feedback=0.3,
reverb=0.2, reverb_type="plate")
reverb=0.2, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", instrument="upright_bass",
volume=0.45, pan=0.0, lowpass=500)
volume=0.45, pan=0.0, lowpass=500,
humanize=0.2, reverb=0.2)
for sym in ["C", "Am", "F", "G"] * 2:
organ.add(Chord.from_symbol(sym), Duration.WHOLE)
for n, d in [
("E5",.67),("G5",.33),("C6",1),("B5",.67),("A5",.33),
("G5",1),(None,1),
("A5",.67),("C6",.33),("E5",1),("D5",.67),("C5",.33),
("A4",1),(None,1),
("F5",.67),("A5",.33),("C6",1),("B5",.67),("A5",.33),
("G5",.67),("E5",.33),("C5",1),(None,1),
("D5",.67),("E5",.33),("G5",.67),("B5",.33),
("C6",2),(None,2),
for n, d, v in [
("E5",.67,82),("G5",.33,78),("C6",1,92),("B5",.67,85),("A5",.33,80),
("G5",1,82),(None,1,0),
("A5",.67,85),("C6",.33,88),("E5",1,80),("D5",.67,78),("C5",.33,72),
("A4",1,75),(None,1,0),
("F5",.67,82),("A5",.33,85),("C6",1,92),("B5",.67,88),("A5",.33,82),
("G5",.67,80),("E5",.33,75),("C5",1,72),(None,1,0),
("D5",.67,78),("E5",.33,80),("G5",.67,85),("B5",.33,88),
("C6",2,92),(None,2,0),
# Second half: more intense
("C6",.67),("B5",.33),("A5",.67),("G5",.33),
("E5",1),("C5",.67),("E5",.33),
("A5",1),("G5",.67),("E5",.33),("C5",1),(None,1),
("F5",1),("A5",.67),("C6",.33),("E6",2),
("D6",.67),("C6",.33),("B5",.67),("G5",.33),
("C6",3),(None,1),
("C6",.67,95),("B5",.33,88),("A5",.67,90),("G5",.33,82),
("E5",1,85),("C5",.67,78),("E5",.33,82),
("A5",1,90),("G5",.67,85),("E5",.33,78),("C5",1,75),(None,1,0),
("F5",1,85),("A5",.67,90),("C6",.33,95),("E6",2,100),
("D6",.67,92),("C6",.33,88),("B5",.67,90),("G5",.33,82),
("C6",3,95),(None,1,0),
]:
lead.rest(d) if n is None else lead.add(n, d)
lead.rest(d) if n is None else lead.add(n, d, velocity=v)
for n in ["C2","E2","G2","C3","A1","C2","E2","A2",
"F2","A2","C3","F2","G2","B2","D3","G2"] * 2:
@@ -664,7 +688,8 @@ def dub_delay_madness():
reverb=0.7, reverb_decay=3.0, reverb_type="cathedral",
lowpass=1200, detune=8, humanize=0.2)
bass = score.part("bass", instrument="bass_guitar",
volume=0.6, pan=0.0, lowpass=350, lowpass_q=1.5)
volume=0.6, pan=0.0, lowpass=350, lowpass_q=1.5,
humanize=0.2)
siren = score.part("siren", synth="pwm_slow", envelope="pad",
volume=0.12, pan=0.5,
reverb=0.8, reverb_decay=4.0, reverb_type="cathedral",
@@ -674,7 +699,8 @@ def dub_delay_madness():
melodica = score.part("melodica", instrument="flute",
volume=0.35, pan=0.3,
delay=0.6, delay_time=0.66, delay_feedback=0.55,
reverb=0.5, reverb_decay=2.5, reverb_type="cathedral")
reverb=0.5, reverb_decay=2.5, reverb_type="cathedral",
humanize=0.2)
for sym in ["Em", "Em", "Am", "Am", "Em", "Em", "Bm", "Em"]:
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
@@ -684,13 +710,13 @@ def dub_delay_madness():
bass.add(n, Duration.HALF)
# Melodica: very sparse — let the delay do the work
for n, d in [
("E5", 1.5), (None, 6.5),
("G5", 1), ("A5", 1), (None, 6),
(None, 4), ("B5", 2), (None, 6),
("E5", 1), (None, 3), ("D5", 1.5), (None, 2.5),
for n, d, v in [
("E5", 1.5, 78), (None, 6.5, 0),
("G5", 1, 82), ("A5", 1, 85), (None, 6, 0),
(None, 4, 0), ("B5", 2, 88), (None, 6, 0),
("E5", 1, 75), (None, 3, 0), ("D5", 1.5, 72), (None, 2.5, 0),
]:
melodica.rest(d) if n is None else melodica.add(n, d)
melodica.rest(d) if n is None else melodica.add(n, d, velocity=v)
# Siren: long notes that disappear into the void
for n, d in [
@@ -718,25 +744,27 @@ def drum_and_bass():
lead = score.part("lead", instrument="flute",
volume=0.4, pan=0.3,
delay=0.3, delay_time=0.172, delay_feedback=0.4,
reverb=0.25, reverb_type="plate")
reverb=0.25, reverb_type="plate",
humanize=0.2)
bass = score.part("bass", instrument="808_bass",
volume=0.55, pan=0.0)
volume=0.55, pan=0.0,
reverb=0.1)
for sym in ["Am", "F", "C", "G"] * 2:
pads.add(Chord.from_symbol(sym), Duration.WHOLE)
# Liquid melody — flowing, emotional
for n, d in [
("A5", 1), ("G5", .5), ("E5", .5), ("C5", 1), (None, 1),
("D5", .5), ("E5", .5), ("G5", 1), ("A5", 1.5), (None, .5),
("C6", 1), ("B5", .5), ("A5", .5), ("G5", 1), ("E5", 1),
("F5", .5), ("G5", .5), ("A5", 1.5), (None, .5), ("G5", 1),
("A5", 1), ("G5", .5), ("E5", .5), ("C5", 1), (None, 1),
("E5", .5), ("G5", .5), ("B5", 1), ("A5", 2),
("G5", 1), ("E5", .5), ("D5", .5), ("C5", 1.5), (None, .5),
("E5", .5), ("G5", .5), ("A5", 2), (None, 1),
for n, d, v in [
("A5", 1, 85), ("G5", .5, 78), ("E5", .5, 72), ("C5", 1, 75), (None, 1, 0),
("D5", .5, 72), ("E5", .5, 78), ("G5", 1, 82), ("A5", 1.5, 88), (None, .5, 0),
("C6", 1, 92), ("B5", .5, 85), ("A5", .5, 80), ("G5", 1, 82), ("E5", 1, 75),
("F5", .5, 78), ("G5", .5, 82), ("A5", 1.5, 88), (None, .5, 0), ("G5", 1, 80),
("A5", 1, 85), ("G5", .5, 78), ("E5", .5, 72), ("C5", 1, 75), (None, 1, 0),
("E5", .5, 78), ("G5", .5, 82), ("B5", 1, 90), ("A5", 2, 85),
("G5", 1, 80), ("E5", .5, 75), ("D5", .5, 70), ("C5", 1.5, 72), (None, .5, 0),
("E5", .5, 78), ("G5", .5, 82), ("A5", 2, 88), (None, 1, 0),
]:
lead.rest(d) if n is None else lead.add(n, d)
lead.rest(d) if n is None else lead.add(n, d, velocity=v)
# Sub bass — half note roots, deep
for n in ["A1","A1","F1","F1","C1","C1","G1","G1"] * 2:
@@ -762,7 +790,8 @@ def drake_vibes():
bells = score.part("bells", instrument="vibraphone",
volume=0.3, pan=0.4,
reverb=0.4, reverb_decay=2.0, reverb_type="plate",
delay=0.25, delay_time=0.44, delay_feedback=0.35)
delay=0.25, delay_time=0.44, delay_feedback=0.35,
humanize=0.2)
lead = score.part("lead", synth="pwm_slow", envelope="strings",
volume=0.35, pan=-0.2,
reverb=0.3, reverb_type="cathedral", lowpass=2000,
@@ -994,10 +1023,12 @@ def dance_party():
bass = score.part("bass", instrument="synth_bass",
volume=0.45, lowpass=500, lowpass_q=1.3,
sidechain=0.75, sidechain_release=0.12)
sidechain=0.75, sidechain_release=0.12,
reverb=0.15)
sparkle = score.part("sparkle", instrument="vibraphone",
volume=0.3, pan=0.4, reverb=0.3, reverb_decay=1.5,
delay=0.2, delay_time=0.234, delay_feedback=0.3)
delay=0.2, delay_time=0.234, delay_feedback=0.3,
humanize=0.2)
chords_part = score.part("chords", instrument="synth_pad",
volume=0.2,
reverb=0.4, reverb_type="plate", sidechain=0.7)
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.36.0"
version = "0.36.2"
description = "Music Theory for Humans"
readme = "README.md"
license = "MIT"
+1 -1
View File
@@ -1,6 +1,6 @@
"""PyTheory: Music Theory for Humans."""
__version__ = "0.36.0"
__version__ = "0.36.2"
from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET
+52 -1
View File
@@ -299,6 +299,54 @@ def cmd_demo(args):
"lead": ("trumpet_synth", "bowed", 0.3, 0.2),
"pad": ("piano_synth", "none", -0.2),
"bass_lp": 600, "reverb_type": "plate"},
{"name": "Theremin Noir", "key": ("A", "minor"), "drums": "hip hop",
"fill": "rock", "bpm": 85,
"prog": ("i", "iv", "V", "i"),
"lead": ("theremin_synth", "pad", 0.4, 0.0),
"pad": ("strings_synth", "pad", 0.0),
"bass_lp": 300, "reverb_type": "cave"},
{"name": "Caribbean", "key": ("C", "major"), "drums": "reggae",
"fill": "reggae", "bpm": 110,
"prog": ("I", "IV", "V", "IV"),
"lead": ("steel_drum_synth", "none", 0.25, 0.3),
"pad": ("acoustic_guitar_synth", "none", -0.3),
"bass_lp": 500, "reverb_type": "plate"},
{"name": "Accordion Waltz", "key": ("D", "minor"), "drums": "waltz",
"fill": "jazz", "bpm": 88,
"prog": ("i", "iv", "V", "i"),
"lead": ("accordion_synth", "organ", 0.2, 0.1),
"pad": ("strings_synth", "pad", -0.2),
"bass_lp": 500, "reverb_type": "plate"},
{"name": "Kalimba Dreams", "key": ("G", "major"), "drums": "cajon folk",
"fill": "bossa nova", "bpm": 95,
"prog": ("I", "vi", "IV", "V"),
"lead": ("kalimba_synth", "none", 0.35, 0.2),
"pad": ("piano_synth", "none", -0.2),
"bass_lp": 400, "reverb_type": "taj_mahal"},
{"name": "Outback Drone", "key": ("E", "minor"), "drums": "djembe",
"fill": "afrobeat", "bpm": 70,
"prog": ("i", "iv", "i", "V"),
"lead": ("didgeridoo_synth", "pad", 0.3, 0.0),
"pad": ("granular_synth", "pad", 0.0),
"bass_lp": 200, "reverb_type": "cave"},
{"name": "Highland", "key": ("A", "minor"), "drums": "flamenco",
"fill": "rock", "bpm": 95,
"prog": ("i", "iv", "V", "i"),
"lead": ("bagpipe_synth", "organ", 0.15, 0.0),
"pad": ("strings_synth", "pad", -0.2),
"bass_lp": 400, "reverb_type": "cathedral"},
{"name": "Nashville Tears", "key": ("G", "major"), "drums": "country",
"fill": "rock", "bpm": 85,
"prog": ("I", "IV", "V", "IV"),
"lead": ("pedal_steel_synth", "strings", 0.35, 0.2),
"pad": ("piano_synth", "none", -0.3),
"bass_lp": 500, "reverb_type": "spring"},
{"name": "Tabla Fusion", "key": ("E", "minor"), "drums": "teental",
"fill": "rock", "bpm": 120,
"prog": ("i", "iv", "V", "i"),
"lead": ("sitar_synth", "none", 0.3, 0.2),
"pad": ("vocal_synth", "pad", 0.0),
"bass_lp": 400, "reverb_type": "taj_mahal"},
]
mood = random.choice(moods)
@@ -375,7 +423,10 @@ def cmd_demo(args):
print(f" {mood['drums']} | {lead_synth} lead | {pad_synth} pad | {mood['reverb_type']} reverb")
print()
play_score(score)
try:
play_score(score)
except KeyboardInterrupt:
pass
print("")
+243 -4
View File
@@ -1130,6 +1130,228 @@ def granular_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
return (peak * out).astype(numpy.int16)
def pedal_steel_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Pedal steel guitar — the Nashville crying sound.
Sustained steel string with natural portamento character,
very smooth, lots of harmonics, and a singing quality from
the bar sliding on the strings.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Slow, singing vibrato — the bar wobbling on the strings
vib = hz * 0.002 * numpy.sin(2 * numpy.pi * 4.0 * t)
# Rich harmonics — steel bar gives a clear, singing tone
wave = numpy.zeros(n_samples, dtype=numpy.float64)
for n in range(1, 12):
f_n = hz * n
if f_n >= SAMPLE_RATE / 2:
break
amp = 1.0 / n * numpy.exp(-0.08 * n)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * (f_n + vib * n) * t + phase)
# Long sustain envelope
wave *= numpy.exp(-0.8 * t)
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def theremin_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Theremin — pure sine with natural wobble.
The theremin's sound is a nearly pure sine wave with slight
pitch instability from hand position. The eerie, sci-fi sound
comes from this purity combined with continuous pitch.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
# Natural hand wobble — slightly irregular vibrato
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
wobble = hz * 0.004 * numpy.sin(2 * numpy.pi * 5.8 * t)
wobble += hz * 0.001 * rng.normal(0, 1, n_samples)
wave = numpy.sin(2 * numpy.pi * (hz + wobble) * t)
# Slight 2nd harmonic — real theremins aren't perfectly pure
wave += 0.08 * numpy.sin(2 * numpy.pi * (hz * 2 + wobble * 2) * t)
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def kalimba_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Kalimba/thumb piano — metal tines on a wooden body.
Bright, bell-like attack with inharmonic overtones from the
metal tines. The wooden resonator gives warmth underneath.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
# Metal tine modes — slightly inharmonic like marimba
wave = numpy.sin(2 * numpy.pi * hz * t) * 0.8
wave += numpy.sin(2 * numpy.pi * hz * 2.92 * t) * 0.25 * numpy.exp(-12 * t)
wave += numpy.sin(2 * numpy.pi * hz * 5.4 * t) * 0.1 * numpy.exp(-20 * t)
# Two-stage decay: bright attack dies fast, fundamental rings
decay = numpy.where(t < 0.1,
numpy.exp(-3 * t),
numpy.exp(-3 * 0.1) * numpy.exp(-1.5 * (t - 0.1)))
wave *= decay
# Wooden body resonance
import scipy.signal as _sig
for center, bw, gain in [(300, 100, 0.2), (600, 120, 0.15)]:
lo, hi = max(20, center - bw), min(SAMPLE_RATE // 2 - 1, center + bw)
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
wave += _sig.lfilter(bp, ap, wave) * gain
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def steel_drum_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Steel drum/pan — hammered metal with bright, ringing tone.
The steel pan has specific inharmonic partials from the
hand-hammered notes. Bright, tropical, bell-like.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
# Steel pan modes — distinctly metallic
wave = numpy.sin(2 * numpy.pi * hz * t) * 0.7
wave += numpy.sin(2 * numpy.pi * hz * 2.0 * t) * 0.4 * numpy.exp(-5 * t)
wave += numpy.sin(2 * numpy.pi * hz * 3.01 * t) * 0.25 * numpy.exp(-8 * t)
wave += numpy.sin(2 * numpy.pi * hz * 4.1 * t) * 0.15 * numpy.exp(-12 * t)
wave += numpy.sin(2 * numpy.pi * hz * 5.3 * t) * 0.08 * numpy.exp(-18 * t)
# Mallet impact
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
hit_len = min(int(SAMPLE_RATE * 0.008), n_samples)
hit = rng.uniform(-0.2, 0.2, hit_len) * numpy.exp(-numpy.linspace(0, 12, hit_len))
wave[:hit_len] += hit
# Two-stage decay
decay = numpy.where(t < 0.15,
numpy.exp(-4 * t),
numpy.exp(-4 * 0.15) * numpy.exp(-1.2 * (t - 0.15)))
wave *= decay
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def accordion_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Accordion — bellows-driven free reeds.
Two reeds per note slightly detuned (musette tuning) create
the characteristic beating/tremolo. Rich in harmonics from
the reed vibration.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Two reeds slightly detuned — musette beating
detune_cents = 8
hz2 = hz * (2 ** (detune_cents / 1200))
# Reed harmonics — rich, like a square-ish wave but warmer
wave = numpy.zeros(n_samples, dtype=numpy.float64)
for reed_hz in [hz, hz2]:
for n in range(1, 10):
f_n = reed_hz * n
if f_n >= SAMPLE_RATE / 2:
break
# Odd harmonics stronger (reed character)
amp = (1.0 / n) * (1.2 if n % 2 == 1 else 0.6)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase)
wave *= 0.5 # normalize for two reeds
# Bellows pressure variation — slow amplitude swell
bellows = 0.85 + 0.15 * numpy.sin(2 * numpy.pi * 0.8 * t)
wave *= bellows
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def didgeridoo_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Didgeridoo — circular breathing drone through a wooden tube.
Deep fundamental with strong odd harmonics from the cylindrical
bore. The overtone singing technique creates shifting formants.
Buzzy, droning, primal.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Lip buzz source — rich, raw
phase = numpy.cumsum(2 * numpy.pi * hz / SAMPLE_RATE * numpy.ones(n_samples))
buzz = numpy.zeros(n_samples, dtype=numpy.float64)
for n in range(1, 15):
if hz * n >= SAMPLE_RATE / 2:
break
# Odd harmonics stronger (cylindrical bore, like clarinet)
amp = (1.0 / n) * (1.0 if n % 2 == 1 else 0.4)
buzz += amp * numpy.sin(phase * n + rng.uniform(0, 2 * numpy.pi))
# Shifting formant — the overtone singing effect
# Sweeps slowly between 500Hz and 1500Hz
formant_center = 800 + 400 * numpy.sin(2 * numpy.pi * 0.3 * t)
import scipy.signal as _sig
# Block-process the formant sweep
block = 2048
out = numpy.zeros(n_samples, dtype=numpy.float64)
for i in range(0, n_samples, block):
end = min(i + block, n_samples)
fc = formant_center[(i + end) // 2]
lo = max(20, int(fc - 300))
hi = min(SAMPLE_RATE // 2 - 1, int(fc + 300))
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
seg = _sig.lfilter(bp, ap, buzz[i:end])
out[i:end] = buzz[i:end] * 0.5 + seg * 0.5
else:
out[i:end] = buzz[i:end]
# Breath noise
breath = rng.normal(0, 0.04, n_samples)
out += breath
mx = numpy.abs(out).max()
if mx > 0:
out /= mx
return (peak * out).astype(numpy.int16)
def bagpipe_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Bagpipes — chanter reed with constant drone pressure.
The chanter (melody pipe) uses a double reed like an oboe
but with more buzz and brightness. The constant air pressure
from the bag means no dynamics — always ff.
"""
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Chanter — all harmonics, bright and reedy
wave = numpy.zeros(n_samples, dtype=numpy.float64)
for n in range(1, 18):
f_n = hz * n
if f_n >= SAMPLE_RATE / 2:
break
# Peaked around harmonics 3-7 (the piercing brightness)
amp = (1.0 / n) * numpy.exp(-0.03 * (n - 5) ** 2)
phase = rng.uniform(0, 2 * numpy.pi)
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase)
# Reed buzz — more than oboe
reed = rng.normal(0, 0.08, n_samples)
import scipy.signal as _sig
lo = max(20, int(hz * 2))
hi = min(SAMPLE_RATE // 2 - 1, int(hz * 8))
if lo < hi:
br, ar = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
reed = _sig.lfilter(br, ar, reed).astype(numpy.float64) * 1.5
wave += reed
# Bag pressure wobble — very subtle
bag = 1.0 + 0.02 * numpy.sin(2 * numpy.pi * 1.5 * t)
wave *= bag
mx = numpy.abs(wave).max()
if mx > 0:
wave /= mx
return (peak * wave).astype(numpy.int16)
def banjo_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Banjo — steel strings on a drum-head body.
@@ -1519,8 +1741,11 @@ def _play_for(sample_wave, ms):
"""Play the given NumPy sample array through the speakers."""
normalized_wave = sample_wave.astype(numpy.float32) / SAMPLE_PEAK
_sd = _get_sd()
_sd.play(normalized_wave, SAMPLE_RATE)
_sd.wait()
try:
_sd.play(normalized_wave, SAMPLE_RATE)
_sd.wait()
except KeyboardInterrupt:
_sd.stop()
class Synth(Enum):
@@ -1565,6 +1790,13 @@ class Synth(Enum):
SAXOPHONE = "saxophone_synth"
GRANULAR = "granular_synth"
VOCAL = "vocal_synth"
PEDAL_STEEL = "pedal_steel_synth"
THEREMIN = "theremin_synth"
KALIMBA = "kalimba_synth"
STEEL_DRUM = "steel_drum_synth"
ACCORDION = "accordion_synth"
DIDGERIDOO = "didgeridoo_synth"
BAGPIPE = "bagpipe_synth"
BANJO = "banjo_synth"
MANDOLIN = "mandolin_synth"
UKULELE = "ukulele_synth"
@@ -1591,6 +1823,10 @@ _SYNTH_FUNCTIONS = {
"harp_synth": harp_wave, "upright_bass_synth": upright_bass_wave,
"timpani_synth": timpani_wave, "saxophone_synth": saxophone_wave,
"granular_synth": granular_wave, "vocal_synth": vocal_wave,
"pedal_steel_synth": pedal_steel_wave, "theremin_synth": theremin_wave,
"kalimba_synth": kalimba_wave, "steel_drum_synth": steel_drum_wave,
"accordion_synth": accordion_wave, "didgeridoo_synth": didgeridoo_wave,
"bagpipe_synth": bagpipe_wave,
"banjo_synth": banjo_wave, "mandolin_synth": mandolin_wave,
"ukulele_synth": ukulele_wave,
"acoustic_guitar_synth": acoustic_guitar_wave,
@@ -4401,8 +4637,11 @@ def play_score(score):
"""
buf = render_score(score)
_sd = _get_sd()
_sd.play(buf, SAMPLE_RATE)
_sd.wait()
try:
_sd.play(buf, SAMPLE_RATE)
_sd.wait()
except KeyboardInterrupt:
_sd.stop()
# ── MIDI export ─────────────────────────────────────────────────────────────
+176 -7
View File
@@ -77,6 +77,7 @@ def cmd_help(session, args):
Parts:
part lead saw pluck score.part("lead", synth="saw", envelope="pluck")
part bass sine score.part("bass", synth="sine")
part lead instrument piano score.part("lead", instrument="piano")
part list all parts
Notes (on active part):
@@ -85,6 +86,12 @@ def cmd_help(session, args):
rest 2 part.rest(2.0)
arp Am updown 2 2 part.arpeggio("Am", pattern="updown", bars=2, octaves=2)
prog I V vi IV part adds key.progression(...)
strum Am 2 down part.strum("Am", 2, direction="down")
strum G 2 up 0.1 lazy strum (strum_time=0.1)
roll C3 4 part.roll("C3", 4) timpani/tremolo
roll C3 4 30 110 roll with velocity ramp
bend C5 1 2 part.add("C5", 1, bend=2) bend up 2 semitones
bend C5 1 -1 bend down a half step
Effects (on active part):
reverb 0.4 reverb=0.4
@@ -110,6 +117,12 @@ def cmd_help(session, args):
fingering Am guitar chord fingering
diagram [mode] [frets] scale diagram on guitar
Tuning:
temperament equal set temperament (equal/pythagorean/meantone/just)
temperament show current temperament
reference 432 set reference pitch (default 440)
instruments list all available instruments
Session:
show score info
status current state
@@ -197,12 +210,22 @@ def cmd_part(session, args):
return
name = args[0]
synth = args[1] if len(args) > 1 else "saw"
envelope = args[2] if len(args) > 2 else "pluck"
if name not in session.parts:
session.parts[name] = session.score.part(name, synth=synth, envelope=envelope)
print(f" score.part(\"{name}\", synth=\"{synth}\", envelope=\"{envelope}\")")
# Check if second arg is "instrument" keyword or an instrument name
if len(args) > 1 and args[1] == "instrument" and len(args) > 2:
instrument = args[2]
session.parts[name] = session.score.part(name, instrument=instrument)
print(f" score.part(\"{name}\", instrument=\"{instrument}\")")
elif len(args) > 1 and args[1] in _INSTRUMENT_NAMES:
instrument = args[1]
session.parts[name] = session.score.part(name, instrument=instrument)
print(f" score.part(\"{name}\", instrument=\"{instrument}\")")
else:
synth = args[1] if len(args) > 1 else "saw"
envelope = args[2] if len(args) > 2 else "pluck"
session.parts[name] = session.score.part(name, synth=synth, envelope=envelope)
print(f" score.part(\"{name}\", synth=\"{synth}\", envelope=\"{envelope}\")")
else:
print(f"{name}")
session.current_part = session.parts[name]
@@ -534,6 +557,97 @@ def cmd_identify(session, args):
print(f" error: {e}")
def cmd_strum(session, args):
"""Strum a chord on a fretboard-equipped part."""
if not args:
print(" usage: strum Am [beats] [down|up] [strum_time]")
return
part = _require_part(session)
chord_name = args[0]
beats = float(args[1]) if len(args) > 1 else 1.0
direction = args[2] if len(args) > 2 else "down"
strum_time = float(args[3]) if len(args) > 3 else 0.05
try:
part.strum(chord_name, beats, direction=direction, strum_time=strum_time)
print(f" .strum(\"{chord_name}\", {beats}, direction=\"{direction}\", "
f"strum_time={strum_time})")
except Exception as e:
print(f" error: {e}")
def cmd_roll(session, args):
"""Play a roll (rapid repeated notes with velocity ramp)."""
if not args:
print(" usage: roll C3 [beats] [vel_start] [vel_end]")
return
part = _require_part(session)
tone = args[0]
beats = float(args[1]) if len(args) > 1 else 4.0
vel_start = int(args[2]) if len(args) > 2 else 40
vel_end = int(args[3]) if len(args) > 3 else 100
try:
part.roll(tone, beats, velocity_start=vel_start, velocity_end=vel_end)
print(f" .roll(\"{tone}\", {beats}, velocity_start={vel_start}, "
f"velocity_end={vel_end})")
except Exception as e:
print(f" error: {e}")
def cmd_bend(session, args):
"""Add a note with pitch bend."""
if len(args) < 3:
print(" usage: bend C5 1 2 (note, beats, semitones)")
print(" bend C5 1 -1 (bend down)")
return
part = _require_part(session)
note = args[0]
beats = float(args[1])
bend = float(args[2])
bend_type = args[3] if len(args) > 3 else "smooth"
try:
part.add(note, beats, bend=bend, bend_type=bend_type)
print(f" .add(\"{note}\", {beats}, bend={bend}, bend_type=\"{bend_type}\")")
except Exception as e:
print(f" error: {e}")
def cmd_temperament(session, args):
"""Set or show the tuning temperament."""
if not args:
temp = getattr(session.score, 'temperament', 'equal')
ref = getattr(session.score, 'reference_pitch', 440.0)
print(f" temperament={temp} reference={ref} Hz")
print(f" available: equal, pythagorean, meantone, just")
return
temp = args[0]
valid = ["equal", "pythagorean", "meantone", "just"]
if temp not in valid:
print(f" unknown temperament: {temp}")
print(f" available: {', '.join(valid)}")
return
session.score.temperament = temp
print(f" temperament={temp}")
def cmd_reference(session, args):
"""Set the reference pitch (A4 frequency)."""
if not args:
ref = getattr(session.score, 'reference_pitch', 440.0)
print(f" reference={ref} Hz")
return
ref = float(args[0])
session.score.reference_pitch = ref
print(f" reference={ref} Hz")
def cmd_instruments(session, args):
"""List all available instruments."""
cols = 3
for i in range(0, len(_INSTRUMENT_NAMES), cols):
row = _INSTRUMENT_NAMES[i:i + cols]
print(" " + " ".join(f"{name:<22s}" for name in row))
def cmd_circle(session, args):
"""Show circle of fifths."""
tonic = args[0] if args else session.key.tonic_name
@@ -560,7 +674,10 @@ def cmd_clear(session, args):
def cmd_status(session, args):
parts = ", ".join(session.parts.keys()) if session.parts else "none"
active = session.current_part.name if session.current_part else "none"
temp = getattr(session.score, 'temperament', 'equal')
ref = getattr(session.score, 'reference_pitch', 440.0)
print(f" key={session.key} bpm={session.bpm} swing={session.swing}")
print(f" temperament={temp} reference={ref} Hz")
print(f" drums={session._drum_preset or 'none'} parts=[{parts}] active={active}")
@@ -607,6 +724,12 @@ COMMANDS = {
"interval": cmd_interval,
"identify": cmd_identify, "id": cmd_identify,
"circle": cmd_circle,
"strum": cmd_strum,
"roll": cmd_roll,
"bend": cmd_bend,
"temperament": cmd_temperament, "temp": cmd_temperament,
"reference": cmd_reference, "ref": cmd_reference,
"instruments": cmd_instruments,
"clear": cmd_clear,
"status": cmd_status,
}
@@ -653,9 +776,43 @@ def _prompt(session):
# ── Tab completion ─────────────────────────────────────────────────────────
_SYNTH_NAMES = ["sine", "saw", "triangle", "square", "pulse", "fm",
"noise", "supersaw", "pwm_slow", "pwm_fast"]
"noise", "supersaw", "pwm_slow", "pwm_fast",
"pedal_steel_synth", "theremin_synth", "kalimba_synth",
"steel_drum_synth", "accordion_synth", "didgeridoo_synth",
"bagpipe_synth", "banjo_synth", "mandolin_synth",
"ukulele_synth", "vocal_synth", "granular_synth",
"piano_synth", "organ_synth", "harpsichord_synth",
"strings_synth", "cello_synth", "flute_synth",
"clarinet_synth", "oboe_synth", "trumpet_synth",
"acoustic_guitar_synth", "electric_guitar_synth",
"bass_guitar_synth", "upright_bass_synth", "harp_synth",
"sitar_synth", "pluck_synth", "saxophone_synth",
"marimba_synth", "timpani_synth"]
_INSTRUMENT_NAMES = [
# Keys
"piano", "electric_piano", "organ", "harpsichord", "celesta", "music_box",
# Strings
"violin", "viola", "cello", "contrabass", "string_ensemble",
# Woodwinds
"flute", "clarinet", "oboe", "bassoon",
# Brass
"trumpet", "trombone", "french_horn", "tuba", "brass_ensemble",
# Plucked
"acoustic_guitar", "electric_guitar", "clean_guitar", "crunch_guitar",
"distorted_guitar", "orange_crunch", "metal_guitar", "bass_guitar",
"upright_bass", "harp", "sitar", "pedal_steel", "theremin", "kalimba",
"steel_drum", "accordion", "didgeridoo", "bagpipe", "banjo", "mandolin",
"mandola", "ukulele", "koto",
# Synth presets
"synth_lead", "synth_pad", "synth_bass", "acid_bass",
"granular_pad", "vocal", "choir", "granular_texture", "808_bass",
# Percussion / Mallet
"vibraphone", "marimba", "xylophone", "glockenspiel", "tubular_bells", "timpani",
# Woodwinds (continued)
"saxophone", "alto_sax", "tenor_sax", "bari_sax",
]
_ENVELOPE_NAMES = ["piano", "pluck", "pad", "organ", "bell", "strings",
"staccato", "none"]
"staccato", "bowed", "mallet", "none"]
_ARP_PATTERNS = ["up", "down", "updown", "downup", "random"]
_LFO_SHAPES = ["sine", "triangle", "saw", "square"]
_SYSTEMS = ["western", "indian", "arabic", "japanese", "blues", "gamelan"]
@@ -667,7 +824,7 @@ _CHORD_SUFFIXES = ["", "m", "7", "m7", "maj7", "dim", "aug", "sus2", "sus4",
# Context-aware completions for the second word
_ARG_COMPLETIONS = {
"drums": lambda: Pattern.list_presets(),
"part": lambda: _SYNTH_NAMES,
"part": lambda: _SYNTH_NAMES + _INSTRUMENT_NAMES,
"key": lambda: [f"{n}m" for n in _NOTE_NAMES[:12]] + _NOTE_NAMES[:12],
"arp": lambda: [f"{n}{s}" for n in _NOTE_NAMES[:7] for s in _CHORD_SUFFIXES[:6]],
"add": lambda: [f"{n}{o}" for n in _NOTE_NAMES[:12] for o in ["3", "4", "5"]],
@@ -679,6 +836,12 @@ _ARG_COMPLETIONS = {
"lowpass_q", "reverb_decay", "delay_time", "delay_feedback",
"distortion_drive"],
"identify": lambda: [f"{n}{s}" for n in _NOTE_NAMES[:7] for s in _CHORD_SUFFIXES[:6]],
"strum": lambda: [f"{n}{s}" for n in _NOTE_NAMES[:7] for s in _CHORD_SUFFIXES[:6]],
"roll": lambda: [f"{n}{o}" for n in _NOTE_NAMES[:12] for o in ["2", "3", "4", "5"]],
"bend": lambda: [f"{n}{o}" for n in _NOTE_NAMES[:12] for o in ["3", "4", "5"]],
"temperament": lambda: ["equal", "pythagorean", "meantone", "just"],
"reference": lambda: ["440", "432", "415", "444"],
"instruments": lambda: _INSTRUMENT_NAMES,
}
@@ -705,6 +868,12 @@ def _completer(text, state):
elif cmd == "arp" and len(tokens) == 3:
# Pattern for arp
options = [p for p in _ARP_PATTERNS if p.startswith(text)]
elif cmd == "strum" and len(tokens) == 4:
# Direction for strum
options = [d for d in ["down", "up"] if d.startswith(text)]
elif cmd == "bend" and len(tokens) == 5:
# Bend type
options = [t for t in ["smooth", "linear", "late"] if t.startswith(text)]
elif cmd == "lfo" and len(tokens) >= 7:
# Shape for lfo
options = [s for s in _LFO_SHAPES if s.startswith(text)]
+31
View File
@@ -195,6 +195,37 @@ INSTRUMENTS = {
"detune": 12, "lowpass": 3000, "lowpass_q": 1.5,
"humanize": 0.2,
},
"pedal_steel": {
"synth": "pedal_steel_synth", "envelope": "strings",
"reverb": 0.3, "reverb_type": "spring",
"humanize": 0.15,
},
"theremin": {
"synth": "theremin_synth", "envelope": "pad",
"legato": True, "glide": 0.05,
"reverb": 0.3, "reverb_type": "plate",
},
"kalimba": {
"synth": "kalimba_synth", "envelope": "none",
"reverb": 0.35, "reverb_type": "plate",
},
"steel_drum": {
"synth": "steel_drum_synth", "envelope": "none",
"reverb": 0.3, "reverb_type": "plate",
},
"accordion": {
"synth": "accordion_synth", "envelope": "organ",
"humanize": 0.15,
},
"didgeridoo": {
"synth": "didgeridoo_synth", "envelope": "pad",
"lowpass": 1500,
"reverb": 0.4, "reverb_type": "cave",
},
"bagpipe": {
"synth": "bagpipe_synth", "envelope": "organ",
"lowpass": 4000,
},
"banjo": {
"synth": "banjo_synth", "envelope": "none",
"humanize": 0.2,
+240 -1
View File
@@ -5320,7 +5320,7 @@ def test_supersaw_wave():
@needs_portaudio
def test_all_synths_in_enum():
from pytheory.play import Synth
assert len(Synth) == 30
assert len(Synth) == 41
for s in Synth:
wave = s(440, n_samples=1000)
assert len(wave) == 1000
@@ -6912,3 +6912,242 @@ def test_clean_guitar_preset():
p = score.part("g", instrument="clean_guitar")
assert p.synth == "electric_guitar_synth"
assert p.cabinet > 0
# ── New instrument synths (v0.36+) ──────────────────────────────────────────
def test_new_synths_render():
"""All 7 new synths produce valid audio."""
from pytheory.play import (pedal_steel_wave, theremin_wave, kalimba_wave,
steel_drum_wave, accordion_wave,
didgeridoo_wave, bagpipe_wave,
banjo_wave, mandolin_wave, ukulele_wave,
vocal_wave, SAMPLE_RATE)
synths = [pedal_steel_wave, theremin_wave, kalimba_wave, steel_drum_wave,
accordion_wave, didgeridoo_wave, bagpipe_wave,
banjo_wave, mandolin_wave, ukulele_wave, vocal_wave]
for fn in synths:
wave = fn(440, n_samples=11025)
assert len(wave) == 11025
assert wave.dtype == numpy.int16
assert numpy.abs(wave).max() > 0
def test_vocal_synth_with_lyric():
"""Vocal synth accepts lyric parameter."""
from pytheory.play import vocal_wave
for lyric in ["ah", "ee", "oh", "oo", "hi", "la"]:
wave = vocal_wave(330, n_samples=11025, lyric=lyric)
assert len(wave) == 11025
assert numpy.abs(wave).max() > 0
def test_vocal_different_vowels_differ():
"""Different vowels should produce different waveforms."""
from pytheory.play import vocal_wave
ah = vocal_wave(330, n_samples=22050, lyric="ah")
ee = vocal_wave(330, n_samples=22050, lyric="ee")
# They should differ (different formant peaks)
assert not numpy.array_equal(ah, ee)
def test_all_instrument_presets_create():
"""Every instrument preset in INSTRUMENTS should create a valid Part."""
from pytheory import Score
from pytheory.rhythm import INSTRUMENTS
for name in INSTRUMENTS:
score = Score("4/4", bpm=120)
p = score.part("test", instrument=name)
assert p.synth is not None
def test_new_instrument_presets():
"""New instrument presets have correct synths."""
from pytheory import Score
presets = {
"pedal_steel": "pedal_steel_synth",
"theremin": "theremin_synth",
"kalimba": "kalimba_synth",
"steel_drum": "steel_drum_synth",
"accordion": "accordion_synth",
"didgeridoo": "didgeridoo_synth",
"bagpipe": "bagpipe_synth",
"banjo": "banjo_synth",
"mandolin": "mandolin_synth",
"ukulele": "ukulele_synth",
}
for name, expected_synth in presets.items():
score = Score("4/4", bpm=120)
p = score.part("t", instrument=name)
assert p.synth == expected_synth, f"{name} has {p.synth}, expected {expected_synth}"
# ── Cajón drums ─────────────────────────────────────────────────────────────
def test_cajon_sounds_render():
from pytheory.play import _render_drum_hit
from pytheory.rhythm import DrumSound
for sound in [DrumSound.CAJON_BASS, DrumSound.CAJON_SLAP, DrumSound.CAJON_TAP]:
wave = _render_drum_hit(sound.value, 22050)
assert len(wave) == 22050
assert wave.dtype == numpy.float32
def test_cajon_patterns():
from pytheory.rhythm import Pattern
for name in ["cajon", "cajon rumba", "cajon folk"]:
p = Pattern.preset(name)
assert p.beats > 0
# ── Pitch bends ─────────────────────────────────────────────────────────────
def test_pitch_bend_renders():
"""Pitch bend should produce valid audio without errors."""
from pytheory import Score, Duration
from pytheory.play import render_score
score = Score("4/4", bpm=120)
p = score.part("t", instrument="electric_guitar")
p.add("A4", Duration.HALF, bend=2, bend_type="smooth")
p.add("A4", Duration.HALF, bend=-1, bend_type="late")
p.add("A4", Duration.HALF, bend=3, bend_type="linear")
p.add("A4", Duration.HALF)
buf = render_score(score)
assert len(buf) > 0
def test_pitch_bend_types():
"""All three bend types should work."""
from pytheory.rhythm import Note, Duration
for bt in ["smooth", "linear", "late"]:
n = Note(tone=None, duration=Duration.QUARTER, bend=2, bend_type=bt)
assert n.bend_type == bt
# ── Roll method ─────────────────────────────────────────────────────────────
def test_roll_adds_notes():
from pytheory import Score, Duration
score = Score("4/4", bpm=120)
p = score.part("t", instrument="timpani")
p.roll("C3", Duration.WHOLE, velocity_start=30, velocity_end=100)
assert len(p.notes) > 4 # should be many 16th notes
def test_roll_velocity_ramp():
from pytheory import Score, Duration
score = Score("4/4", bpm=120)
p = score.part("t", instrument="timpani")
p.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=100)
velocities = [n.velocity for n in p.notes]
# First should be quieter than last
assert velocities[0] < velocities[-1]
def test_roll_custom_speed():
from pytheory import Score, Duration
score = Score("4/4", bpm=120)
p = score.part("t", synth="sine")
p.roll("A4", Duration.WHOLE, speed=0.125) # 32nd notes
# 4 beats / 0.125 = 32 notes
assert len(p.notes) == 32
# ── Int tone names ──────────────────────────────────────────────────────────
def test_int_tone_name():
from pytheory import Tone, TET
edo = TET(22)
t = Tone(0, octave=4, system=edo)
assert t.name == "0"
assert t.frequency == pytest.approx(440.0, rel=1e-3)
def test_int_tone_wrapping():
from pytheory import Tone, TET
edo = TET(22)
t = Tone(22, octave=4, system=edo)
assert t.name == "0"
assert t.octave == 5
assert t.frequency == pytest.approx(880.0, rel=1e-3)
def test_int_tone_negative():
from pytheory import Tone, TET
edo = TET(22)
t = Tone(-1, octave=4, system=edo)
assert t.name == "21"
assert t.octave == 3
def test_system_tone_method():
from pytheory import TET
edo = TET(19)
t = edo.tone(5, octave=4)
assert t.name == "5"
assert t.octave == 4
# ── B#/Cb octave boundary ──────────────────────────────────────────────────
def test_b_sharp_octave():
t = Tone("B#4")
assert t.octave == 5
assert t.frequency == pytest.approx(Tone("C5").frequency, rel=1e-3)
def test_c_flat_octave():
t = Tone("Cb4")
assert t.octave == 3
assert t.frequency == pytest.approx(Tone("B3").frequency, rel=1e-3)
# ── Note choking ────────────────────────────────────────────────────────────
def test_note_choking_renders():
"""Fast repeated notes should render without errors (choking active)."""
from pytheory import Score, Duration
from pytheory.play import render_score
score = Score("4/4", bpm=200)
p = score.part("t", instrument="piano")
for _ in range(32):
p.add("C4", Duration.SIXTEENTH)
buf = render_score(score)
assert len(buf) > 0
# ── Score system/temperament ───────────────────────────────────────────────
def test_score_temperament():
from pytheory import Score
score = Score("4/4", bpm=120, temperament="just")
assert score.temperament == "just"
def test_score_reference_pitch():
from pytheory import Score
score = Score("4/4", bpm=120, reference_pitch=415.0)
assert score.reference_pitch == 415.0
def test_score_system_propagates():
from pytheory import Score, SYSTEMS
shruti = SYSTEMS["shruti"]
score = Score("4/4", bpm=120, system=shruti)
p = score.part("t", synth="sine")
assert p._system is shruti
# ── Synth enum count ────────────────────────────────────────────────────────
def test_synth_enum_count():
from pytheory.play import Synth
assert len(Synth) == 41
def test_all_synths_render_and_enum_match():
"""Every Synth enum member should render valid audio."""
from pytheory.play import Synth
for s in Synth:
wave = s(440, n_samples=1000)
assert len(wave) == 1000
Generated
+1 -1
View File
@@ -698,7 +698,7 @@ wheels = [
[[package]]
name = "pytheory"
version = "0.36.0"
version = "0.36.2"
source = { editable = "." }
dependencies = [
{ name = "numeral" },