Compare commits

..

6 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
8 changed files with 649 additions and 201 deletions
+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.1"
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.1"
__version__ = "0.36.2"
from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET
+5 -2
View File
@@ -303,7 +303,7 @@ def cmd_demo(args):
"fill": "rock", "bpm": 85,
"prog": ("i", "iv", "V", "i"),
"lead": ("theremin_synth", "pad", 0.4, 0.0),
"pad": ("granular_synth", "pad", 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,
@@ -423,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("")
+10 -4
View File
@@ -1741,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):
@@ -4634,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)]
+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.1"
version = "0.36.2"
source = { editable = "." }
dependencies = [
{ name = "numeral" },