Compare commits

..

18 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
kennethreitz 70efb0ad40 v0.36.0: Banjo, mandolin, ukulele, cajón, vocal synth, granular
34 synth waveforms, 26 songs, vocal/formant synthesis with choir
preset, granular engine, banjo/mandolin/ukulele physical models,
cajón drum with 3 patterns, strum sweep on fretboard instruments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:54:08 -04:00
kennethreitz bf6deaab64 Mandolin synth, cajón drums, Song #26 Acoustic Ensemble
- Mandolin: paired steel strings (natural chorus from doubled
  courses), bright body resonance (500/1000/2000Hz)
- Cajón: bass (woody box thump), slap (snare wire buzz), tap
  (ghost note). 3 patterns: cajon, cajon rumba, cajon folk
- Song #26: guitar + uke + mandolin + cajón — humanized strumming,
  stereo panned, plate reverb
- Mandola preset (mandolin with lowpass for darker tone)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:46:15 -04:00
kennethreitz 7c792c0a2a Ukulele synth + strum sweep on all fretboard instruments
- Ukulele: nylon string KS with small body resonance (350/700/1200Hz),
  faster decay than guitar, mid-heavy character
- Strum sweep: 2 quiet grace notes (25% vel) before the chord hit,
  gives audible strum feel without choppiness
- Default strum_time 0.08 → 0.05 for tighter feel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:36:29 -04:00
kennethreitz bf8d4b9a77 Epic Bhairav: musical polyrhythm section, fix reverb levels
Polyrhythm section uses musical phrases (ti-ra-ki-ta patterns)
in 5-groups, 7-groups, and 9-groups rather than mechanical grid
overlays. Reverb pulled back to 0.4 across the song.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:12:31 -04:00
kennethreitz d2d5115c8a Song #25: Epic Bhairav + vocal synth merged to master
Orchestral piece in 22-shruti JI with choir vowel pads, timpani
rolls, bansuri, cello, sitar, strings, harp, djembe→tabla→extended
tabla solo finale (whisper→ghosts→call/response→9-tuplets→32nd
triplet cascades→grand tihai→slam).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:25:05 -04:00
kennethreitz 3cdd98b158 Merge vocal/formant synth: LF glottal model, 5 formants, choir
Formant synthesis with LF glottal pulse, 5 Peterson & Barney
formant peaks, jitter/shimmer, consonant onsets, click-free
transitions. Presets: vocal, choir.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:22:36 -04:00
kennethreitz 751d5a49b8 Cleaner vocal synth: less static, click-free note transitions
- Jitter reduced (0.3% → 0.1%), shimmer reduced (2% → 0.8%)
- Breath noise halved (0.08 → 0.04), mix 85/15 → 92/8
- 10ms fade in/out on every vocal note prevents clicks
- Smoother syllable transitions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:17:12 -04:00
kennethreitz 6a836dd891 Overhaul vocal synth: LF glottal model, 5 formants, jitter/shimmer
- LF glottal pulse: asymmetric open/close phase (not sines)
- 5 parallel formant filters per vowel (Peterson & Barney data)
- Jitter (0.3% pitch irregularity) + shimmer (2% amplitude)
- Much more voice-like than previous version
- Consonant onsets preserved

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:13:50 -04:00
kennethreitz 1f888e2b21 Vocal/formant synth with choir preset
Formant synthesis: glottal buzz source through parallel bandpass
filters at vowel resonance frequencies. Supports 5 vowels (A E I O U)
with consonant onsets (plosives, sibilants, nasals, fricatives,
liquids, aspirates, glides). Per-note lyrics via Part.add(lyric=).

Best for choir pads — vowel sounds with cathedral reverb and detune.
Consonant synthesis is rudimentary (noise bursts, not real speech).

Presets: vocal (solo), choir (detuned ensemble).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 12:10:54 -04:00
11 changed files with 1716 additions and 218 deletions
+30
View File
@@ -2,6 +2,36 @@
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,
fast decay with membrane resonance
- **Mandolin synth** — paired steel strings (natural chorus from
doubled courses), bright body resonance
- **Ukulele synth** — nylon strings, small mid-heavy body, shorter
sustain than guitar
- **Cajón drums** — bass (woody box thump), slap (snare wire buzz),
tap (ghost note). 3 patterns: cajon, cajon rumba, cajon folk
- **Vocal/formant synth** — LF glottal model, 5 Peterson & Barney
formant peaks, jitter/shimmer, consonant onsets, per-note lyrics.
Presets: vocal, choir
- **Granular synthesis** — grain cloud engine with scatter, pitch
variation, Hanning windows. Presets: granular_pad, granular_texture
- **Strum sweep** — subtle grace notes before chord hit for natural
strum feel on all fretboard instruments
- Mandola preset, 34 synth waveforms, 26 songs
## 0.35.0
- **8.5x faster import** — dropped pytuning/sympy, lazy-load scipy.
+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**30 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
+487 -193
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)
@@ -1315,13 +1346,13 @@ def journey():
# ── Drone — runs the entire piece ──
tanpura = score.part("tanpura", synth="strings_synth", envelope="pad",
detune=3, lowpass=1000, volume=0.12,
reverb=0.5, reverb_type=REV)
reverb=0.6, reverb_type=REV)
for _ in range(40):
tanpura.add("A2", Duration.WHOLE)
# ── Bars 1-8: Piano alone, then cello ──
piano = score.part("piano", instrument="piano", volume=0.35,
reverb=0.35, reverb_type=REV)
reverb=0.6, reverb_type=REV)
for notes in [
["A2","E3","A3","C4","E4","C4","A3","E3"],
["F2","C3","F3","A3","C4","A3","F3","C3"],
@@ -1336,7 +1367,7 @@ def journey():
piano.add(n, Duration.EIGHTH, velocity=68)
cello = score.part("cello", instrument="cello", volume=0.2,
reverb=0.4, reverb_type=REV)
reverb=0.55, reverb_type=REV)
cello.rest(Duration.WHOLE)
for note, dur, vel in [
("A3", 4.0, 55), ("C4", 4.0, 58),
@@ -1347,11 +1378,11 @@ def journey():
# ── Bars 9-16: Harp + oboe + flute + djembe ──
harp = score.part("harp", instrument="harp", volume=0.28,
reverb=0.45, reverb_type=REV)
reverb=0.6, reverb_type=REV)
oboe = score.part("oboe", instrument="oboe", volume=0.22,
reverb=0.4, reverb_type=REV)
reverb=0.55, reverb_type=REV)
flute = score.part("flute", instrument="flute", volume=0.18,
reverb=0.4, reverb_type=REV)
reverb=0.55, reverb_type=REV)
for _ in range(8):
harp.rest(Duration.WHOLE)
for notes in [
@@ -1383,7 +1414,7 @@ def journey():
# ── Bars 15-20: Sitar + tabla ──
sitar = score.part("sitar", instrument="sitar", volume=0.2,
reverb=0.35, reverb_type=REV)
reverb=0.6, reverb_type=REV)
for _ in range(14):
sitar.rest(Duration.WHOLE)
for note, dur, vel in [
@@ -1406,7 +1437,7 @@ def journey():
# Total bars before EDM: 8 piano + 6 harp + 6 djembe + 4 tabla + 9 solo = 33
edm_start = 33
pad = score.part("pad", instrument="synth_pad", volume=0.18,
reverb=0.45, reverb_type=REV,
reverb=0.6, reverb_type=REV,
sidechain=0.6, sidechain_release=0.15)
for _ in range(edm_start):
pad.rest(Duration.WHOLE)
@@ -1551,6 +1582,267 @@ def journey():
play_song(score, "Journey — Piano → World → Sitar EDM (Taj Mahal)")
def epic_bhairav():
"""Epic Bhairav — orchestral + choir + tabla with extended solo finale."""
shruti = SYSTEMS["shruti"]
score = Score("4/4", bpm=90, system=shruti)
REV = "taj_mahal"
T3 = 1.0 / 12.0
T9 = 1.0 / 9.0
ts = TonedScale(system=shruti, tonic=Tone("Sa", octave=4, system=shruti))
bh = list(ts["bhairav"].tones)
S, kR, G, M, P, kD, N, S2 = bh
NA = DrumSound.TABLA_NA
DH = DrumSound.TABLA_DHA
TT = DrumSound.TABLA_TIT
KE = DrumSound.TABLA_KE
GB = DrumSound.TABLA_GE_BEND
GE = DrumSound.TABLA_GE
DJB = DrumSound.DJEMBE_BASS
DJT = DrumSound.DJEMBE_TONE
DJS = DrumSound.DJEMBE_SLAP
# Tanpura
tanpura = score.part("tanpura", synth="strings_synth", envelope="pad",
detune=3, lowpass=900, volume=0.14, reverb=0.4, reverb_type=REV)
tanpura_pa = score.part("tanpura_pa", synth="strings_synth", envelope="pad",
detune=3, lowpass=1200, volume=0.1, reverb=0.4, reverb_type=REV)
sa = Tone("Sa", octave=3, system=shruti)
pa = Tone("Pa", octave=3, system=shruti)
for _ in range(34):
tanpura.add(sa, Duration.WHOLE)
tanpura_pa.add(pa, Duration.WHOLE)
# Timpani
timp = score.part("timp", instrument="timpani")
timp.roll(Tone("Sa", octave=2, system=shruti), Duration.WHOLE,
velocity_start=20, velocity_end=90, speed=0.125)
timp.add(Tone("Sa", octave=2, system=shruti), Duration.HALF, velocity=105)
timp.rest(Duration.HALF)
for _ in range(8):
timp.rest(Duration.WHOLE)
timp.roll(Tone("Sa", octave=2, system=shruti), Duration.WHOLE,
velocity_start=25, velocity_end=115, speed=0.125)
timp.add(Tone("Sa", octave=2, system=shruti), Duration.HALF, velocity=120)
timp.add(Tone("Pa", octave=2, system=shruti), Duration.HALF, velocity=115)
# Choir — bar 3
choir = score.part("choir", synth="vocal_synth", envelope="pad",
detune=8, spread=0.4, reverb=0.4, reverb_type=REV, volume=0.2)
for _ in range(2):
choir.rest(Duration.WHOLE)
for tone, dur, lyric, vel in [
(S, 4.0, "ah", 60), (M, 4.0, "oh", 62), (P, 4.0, "ah", 68),
(S, 4.0, "ee", 65), (kD, 4.0, "oh", 70), (P, 4.0, "ah", 72),
]:
choir.add(tone, dur, velocity=vel, lyric=lyric)
# Bansuri — bar 5
bansuri = score.part("bansuri", instrument="flute", volume=0.22,
reverb=0.4, reverb_type=REV)
for _ in range(4):
bansuri.rest(Duration.WHOLE)
for tone, dur, vel in [
(P, 2.0, 58), (kD, 1.0, 50), (P, 1.0, 55),
(M, 2.0, 55), (G, 1.0, 50), (kR, 1.0, 48), (S, 4.0, 58),
]:
bansuri.add(tone, dur, velocity=vel)
# Cello — bar 3
cello = score.part("cello", instrument="cello", volume=0.22, reverb=0.4, reverb_type=REV)
for _ in range(2):
cello.rest(Duration.WHOLE)
for name, dur, vel in [
("Sa", 4.0, 55), ("Ma", 4.0, 52), ("Pa", 4.0, 58),
("Sa", 4.0, 55), ("komal Dha", 4.0, 58), ("Pa", 4.0, 55),
]:
cello.add(Tone(name, octave=2, system=shruti), dur, velocity=vel)
# Sitar — bar 9
sitar = score.part("sitar", instrument="sitar", volume=0.25, reverb=0.4, reverb_type=REV)
for _ in range(8):
sitar.rest(Duration.WHOLE)
for tone, dur, vel in [
(S, 1.0, 72), (kR, 0.5, 62), (S, 0.5, 68), (G, 2.0, 78),
(M, 1.0, 72), (P, 2.0, 82), (kD, 0.5, 65), (P, 1.0, 75),
(M, 0.5, 65), (G, 0.5, 68), (kR, 0.5, 60), (S, 2.0, 78),
(kR, 0.25, 62), (G, 0.25, 65), (M, 0.25, 70), (P, 0.25, 75),
(kD, 0.25, 70), (N, 0.25, 78), (S2, 0.5, 88),
(N, 0.25, 68), (kD, 0.25, 62), (P, 0.5, 68),
(M, 0.5, 62), (G, 0.5, 65), (kR, 0.5, 58), (S, 2.0, 80),
]:
sitar.add(tone, dur, velocity=vel)
# Strings — bar 13
strings = score.part("strings", instrument="string_ensemble", volume=0.18,
reverb=0.4, reverb_type=REV)
for _ in range(12):
strings.rest(Duration.WHOLE)
for name, dur, vel in [("Sa", 4.0, 58), ("Ma", 4.0, 62), ("Pa", 4.0, 68), ("Sa", 4.0, 72)]:
strings.add(Tone(name, octave=3, system=shruti), dur, velocity=vel)
# Harp — bar 14
harp = score.part("harp", instrument="harp", volume=0.15, reverb=0.4, reverb_type=REV)
for _ in range(13):
harp.rest(Duration.WHOLE)
for name in ["Sa", "komal Ga", "Pa", "Sa", "Pa", "komal Ga", "Sa", "Sa"]:
oct = 4 if name == "Sa" and harp.total_beats > 55 else 3
harp.add(Tone(name, octave=oct, system=shruti), Duration.EIGHTH, velocity=50)
# Drums
silence = Pattern(name="s", time_signature="4/4", beats=16.0, hits=[])
score.add_pattern(silence, repeats=1)
p_dj = Pattern(name="dj", time_signature="4/4", beats=8.0, hits=[
_Hit(DJB, 0.0, 45), _Hit(DJT, 1.0, 38), _Hit(DJT, 1.5, 32),
_Hit(DJS, 2.0, 42), _Hit(DJT, 3.0, 38),
_Hit(DJB, 4.0, 50), _Hit(DJT, 5.0, 42), _Hit(DJT, 5.5, 35),
_Hit(DJS, 6.0, 48), _Hit(DJT, 6.5, 32), _Hit(DJS, 7.0, 45),
])
score.add_pattern(p_dj, repeats=2)
p_tab = Pattern(name="tab", time_signature="4/4", beats=8.0, hits=[
_Hit(DH, 0.0, 82), _Hit(TT, 0.5, 30), _Hit(NA, 1.0, 65),
_Hit(NA, 2.0, 60), _Hit(DH, 3.0, 82),
_Hit(DH, 4.0, 88), _Hit(TT, 4.25, 32), _Hit(TT, 4.5, 35),
_Hit(NA, 5.0, 68), _Hit(TT, 5.5, 30), _Hit(NA, 6.0, 65),
_Hit(DH, 7.0, 88),
])
score.add_pattern(p_tab, repeats=3)
# Extended tabla finale — whisper → ghosts → call/response → blazing
p_f1 = Pattern(name="f1", time_signature="4/4", beats=8.0, hits=[
_Hit(DH, 0.0, 78), _Hit(NA, 2.0, 55),
_Hit(DH, 4.0, 82), _Hit(TT, 5.0, 30), _Hit(NA, 5.5, 52),
_Hit(DH, 7.0, 78),
])
score.add_pattern(p_f1, repeats=1)
p_f2 = Pattern(name="f2", time_signature="4/4", beats=8.0, hits=[
_Hit(DH, 0.0, 95), _Hit(TT, 0.25, 35), _Hit(TT, 0.5, 38),
_Hit(NA, 1.0, 70), _Hit(TT, 1.25, 30), _Hit(NA, 2.0, 65),
_Hit(TT, 2.5, 35), _Hit(DH, 3.0, 90),
_Hit(DH, 4.0, 98), _Hit(TT, 4.25, 38), _Hit(TT, 4.5, 42),
_Hit(NA, 5.0, 75), _Hit(KE, 5.5, 40), _Hit(NA, 6.0, 70),
_Hit(KE, 6.5, 42), _Hit(DH, 7.0, 100), _Hit(GB, 7.5, 92),
])
score.add_pattern(p_f2, repeats=1)
p_f3 = Pattern(name="f3", time_signature="4/4", beats=8.0, hits=[
_Hit(NA, 0.0, 112), _Hit(NA, 0.25, 58), _Hit(TT, 0.5, 40), _Hit(NA, 0.75, 105),
_Hit(GE, 1.0, 105), _Hit(GE, 1.25, 52), _Hit(GB, 1.5, 95), _Hit(GE, 1.75, 48),
_Hit(NA, 2.0, 115), _Hit(TT, 2.125, 32), _Hit(TT, 2.25, 38),
_Hit(NA, 2.5, 108), _Hit(TT, 2.625, 35), _Hit(TT, 2.75, 42),
_Hit(GB, 3.0, 115), _Hit(KE, 3.25, 52), _Hit(GE, 3.5, 70),
_Hit(DH, 4.0, 118),
*[_Hit(TT if i % 2 == 0 else KE, 5.0 + i * T9, 40 + i * 5) for i in range(9)],
_Hit(DH, 7.0, 120),
])
score.add_pattern(p_f3, repeats=1)
# Part 3.5: polyrhythm — space and conversation, not density
T5 = 4.0 / 5.0
p_poly = Pattern(name="poly", time_signature="4/4", beats=16.0, hits=[
# Bar 1: single Dha, let reverb ring. Bayan answers.
_Hit(DH, 0.0, 95),
_Hit(GB, 3.0, 88),
# Bar 2: one 5-group phrase, then breathe
_Hit(NA, 4.0, 75), _Hit(TT, 4.0 + T5, 42),
_Hit(NA, 4.0 + 2*T5, 70), _Hit(TT, 4.0 + 3*T5, 40),
_Hit(DH, 4.0 + 4*T5, 88),
# Bar 3: bayan, pause, one floating 9-group
_Hit(GB, 8.0, 100),
_Hit(NA, 9.0, 62),
*[_Hit(TT if i % 2 == 0 else KE, 10.0 + i * T9, 35 + i * 4)
for i in range(9)],
_Hit(DH, 11.0, 105),
# Bar 4: simple question-answer into sam
_Hit(DH, 12.0, 100), _Hit(NA, 12.5, 62),
_Hit(GE, 13.0, 88),
_Hit(NA, 14.0, 72), _Hit(TT, 14.25, 40), _Hit(NA, 14.5, 70),
_Hit(DH, 15.0, 112), _Hit(GB, 15.5, 105),
])
score.add_pattern(p_poly, repeats=1)
p_f4 = Pattern(name="f4", time_signature="4/4", beats=12.0, hits=[
*[_Hit(TT, 0.0 + i * T3, 38 + i * 2) for i in range(12)],
_Hit(DH, 1.0, 118), _Hit(GB, 1.5, 110),
_Hit(NA, 2.0, 112), _Hit(KE, 2.125, 48), _Hit(NA, 2.25, 108),
_Hit(KE, 2.375, 50), _Hit(NA, 2.5, 110), _Hit(KE, 2.625, 52), _Hit(NA, 2.75, 115),
_Hit(DH, 3.0, 120),
*[_Hit(TT, 3.5 + i * T3, 30 + i * 4) for i in range(18)],
_Hit(DH, 5.0, 122), _Hit(DH, 5.25, 118), _Hit(GB, 5.5, 115),
_Hit(GE, 6.0, 90), _Hit(GE, 7.0, 88),
*[_Hit(NA if i % 3 == 0 else TT, 6.0 + i * (2.0/9.0), 42 + (i%3)*15) for i in range(9)],
_Hit(DH, 8.0, 110), _Hit(NA, 8.25, 75), _Hit(TT, 8.5, 50),
_Hit(KE, 8.75, 55), _Hit(DH, 9.0, 105),
_Hit(DH, 9.25, 115), _Hit(NA, 9.5, 80), _Hit(TT, 9.75, 55),
_Hit(KE, 10.0, 60), _Hit(DH, 10.25, 110),
_Hit(DH, 10.5, 122), _Hit(NA, 10.75, 85), _Hit(TT, 11.0, 60),
_Hit(KE, 11.25, 65), _Hit(DH, 11.5, 127),
_Hit(GB, 11.875, 127),
])
score.add_pattern(p_f4, repeats=1)
score.set_drum_effects(reverb=0.4, reverb_type=REV)
play_song(score, "Epic Bhairav — Orchestra + Choir + Tabla (22-Shruti JI)")
def acoustic_ensemble():
"""Acoustic Ensemble — guitar, ukulele, mandolin, cajón."""
import random
from pytheory import Fretboard
random.seed(7)
score = Score("4/4", bpm=115)
fb_g = Fretboard.guitar()
guitar = score.part("guitar", instrument="acoustic_guitar", fretboard=fb_g,
reverb=0.3, reverb_type="plate", humanize=0.2, pan=-0.3)
fb_u = Fretboard.ukulele()
uke = score.part("uke", instrument="ukulele", fretboard=fb_u,
reverb=0.25, reverb_type="plate", humanize=0.25, pan=0.3)
fb_m = Fretboard.mandolin()
mando = score.part("mando", instrument="mandolin", fretboard=fb_m,
reverb=0.25, reverb_type="plate", humanize=0.2, pan=0.15)
for sym in ["C", "G", "Am", "F"] * 3:
vd = random.randint(75, 95)
vu = random.randint(58, 78)
guitar.strum(sym, Duration.QUARTER, direction="down", velocity=vd)
guitar.strum(sym, Duration.EIGHTH, direction="up", velocity=vu)
guitar.strum(sym, Duration.EIGHTH, direction="down", velocity=vd - 8)
guitar.strum(sym, Duration.QUARTER, direction="up", velocity=vu)
guitar.strum(sym, Duration.QUARTER, direction="down", velocity=vd)
vd2 = random.randint(65, 88)
vu2 = random.randint(50, 72)
uke.rest(Duration.EIGHTH)
uke.strum(sym, Duration.EIGHTH, direction="up", velocity=vu2)
uke.strum(sym, Duration.QUARTER, direction="down", velocity=vd2)
uke.strum(sym, Duration.EIGHTH, direction="up", velocity=vu2)
uke.strum(sym, Duration.EIGHTH, direction="down", velocity=vd2 - 5)
uke.strum(sym, Duration.QUARTER, direction="up", velocity=vu2)
mando.strum(sym, Duration.EIGHTH, direction="down",
velocity=random.randint(65, 82))
mando.strum(sym, Duration.EIGHTH, direction="up",
velocity=random.randint(55, 72))
mando.strum(sym, Duration.EIGHTH, direction="down",
velocity=random.randint(65, 82))
mando.rest(Duration.EIGHTH)
mando.strum(sym, Duration.EIGHTH, direction="up",
velocity=random.randint(55, 72))
mando.strum(sym, Duration.EIGHTH, direction="down",
velocity=random.randint(68, 85))
mando.strum(sym, Duration.QUARTER, direction="down",
velocity=random.randint(70, 85))
score.drums("cajon", repeats=6)
score.set_drum_effects(reverb=0.15)
play_song(score, "Acoustic Ensemble — Guitar, Uke, Mandolin, Cajón")
SONGS = {
"1": ("Bossa Nova in A minor", bossa_nova_girl),
"2": ("Bebop in Bb major", bebop_in_bb),
@@ -1576,6 +1868,8 @@ SONGS = {
"22": ("Greensleeves (Renaissance Lute)", greensleeves),
"23": ("Tabla Solo (Raga Yaman)", tabla_solo_yaman),
"24": ("Journey (Western → World → Indian)", journey),
"25": ("Epic Bhairav (Orchestral + Tabla)", epic_bhairav),
"26": ("Acoustic Ensemble (Guitar+Uke+Mando+Cajón)", acoustic_ensemble),
}
if __name__ == "__main__":
@@ -1589,7 +1883,7 @@ if __name__ == "__main__":
print(f" {key:>2}. {name}")
print()
choice = input(" Pick a song (1-24, or 'all'): ").strip()
choice = input(" Pick a song (1-26, or 'all'): ").strip()
print()
if choice == "all":
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pytheory"
version = "0.35.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.35.1"
__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("")
+601 -7
View File
@@ -909,6 +909,149 @@ def saxophone_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
return (peak * wave).astype(numpy.int16)
def vocal_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE, lyric="ah"):
"""Vocal/formant synthesis — sings vowel sounds at a given pitch.
Models the human voice with:
1. LF glottal model — asymmetric pulse with sharp closure (not just sines)
2. 5 parallel resonant formant filters (real voice has 5 formant peaks)
3. Jitter + shimmer (natural pitch/amplitude irregularity)
4. Aspiration noise mixed with the glottal source
5. Consonant onsets (plosives, sibilants, nasals, etc.)
"""
import scipy.signal as _sig
# 5-formant table: (F1, F2, F3, F4, F5) frequencies and bandwidths
# Based on Peterson & Barney (1952) measurements, male voice
FORMANTS = {
'a': [(800, 130), (1200, 100), (2500, 140), (3300, 250), (3750, 300)],
'e': [(530, 80), (1850, 100), (2500, 130), (3300, 250), (3750, 300)],
'i': [(280, 60), (2250, 100), (2900, 120), (3350, 250), (3750, 300)],
'o': [(500, 100), (1000, 80), (2500, 140), (3300, 250), (3750, 300)],
'u': ((325, 70), (700, 60), (2530, 140), (3300, 250), (3750, 300)),
}
# Formant gains (relative amplitude per formant)
FGAINS = [1.0, 0.8, 0.5, 0.25, 0.15]
rng = numpy.random.default_rng(int(hz * 100 + len(lyric) * 7) % 2**31)
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
# Parse vowels from lyric
vowels_in_lyric = [c.lower() for c in lyric if c.lower() in FORMANTS]
if not vowels_in_lyric:
vowels_in_lyric = ['a']
# ── Glottal source: LF model approximation ──
# Asymmetric pulse: slow open phase, sharp closure, then closed phase.
# Much more "voice-like" than a sine or sawtooth.
# Jitter (pitch irregularity) + shimmer (amplitude irregularity)
jitter = rng.normal(0, hz * 0.001, n_samples) # ~0.1% pitch jitter
shimmer = 1.0 + rng.normal(0, 0.008, n_samples) # ~0.8% amp shimmer
# Vibrato
vib = hz * 0.001 * numpy.sin(2 * numpy.pi * 5.5 * t)
inst_freq = hz + vib + jitter
phase = numpy.cumsum(2 * numpy.pi * inst_freq / SAMPLE_RATE)
# LF glottal shape: sharper falling edge via phase shaping
saw = (phase / (2 * numpy.pi)) % 1.0 # 0 to 1 sawtooth
# Asymmetric: slow rise (60%), fast fall (40%)
glottal = numpy.where(saw < 0.6,
numpy.sin(numpy.pi * saw / 0.6), # smooth rise
-numpy.sin(numpy.pi * (saw - 0.6) / 0.4) * 0.8) # sharp fall
glottal *= shimmer
# Aspiration noise (breathiness) — subtle
breath = rng.normal(0, 0.04, n_samples)
source = glottal * 0.92 + breath * 0.08
# ── Formant filtering ──
n_vowels = len(vowels_in_lyric)
out = numpy.zeros(n_samples, dtype=numpy.float64)
if n_vowels == 1:
# Single vowel — filter the whole thing
formants = FORMANTS[vowels_in_lyric[0]]
for (fc, bw), gain in zip(formants, FGAINS):
lo = max(20, fc - bw)
hi = min(SAMPLE_RATE // 2 - 1, fc + bw)
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
out += _sig.lfilter(bp, ap, source).astype(numpy.float64) * gain
else:
# Multiple vowels — crossfade formants
samples_per_vowel = n_samples // n_vowels
for vi, vowel in enumerate(vowels_in_lyric):
formants = FORMANTS[vowel]
start = vi * samples_per_vowel
end = n_samples if vi == n_vowels - 1 else start + samples_per_vowel
seg = source[start:end].copy()
seg_out = numpy.zeros_like(seg)
for (fc, bw), gain in zip(formants, FGAINS):
lo = max(20, fc - bw)
hi = min(SAMPLE_RATE // 2 - 1, fc + bw)
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
seg_out += _sig.lfilter(bp, ap, seg).astype(numpy.float64) * gain
# Crossfade
fade = min(int(SAMPLE_RATE * 0.02), len(seg_out) // 4)
if vi > 0 and fade > 0:
seg_out[:fade] *= numpy.linspace(0, 1, fade)
if vi < n_vowels - 1 and fade > 0:
seg_out[-fade:] *= numpy.linspace(1, 0, fade)
out[start:end] += seg_out[:end - start]
# ── Consonant onsets ──
lyric_lower = lyric.lower()
if lyric_lower and lyric_lower[0] not in 'aeiou':
c = lyric_lower[0]
cl = min(int(SAMPLE_RATE * 0.035), n_samples)
if c in 'tdkpb':
burst = rng.uniform(-0.5, 0.5, cl) * numpy.exp(-numpy.linspace(0, 18, cl))
out[:cl] = burst + out[:cl] * 0.2
elif c in 'sz':
sib = rng.uniform(-0.4, 0.4, cl)
if cl > 20:
bl, al = _sig.butter(2, [3000, min(8000, SAMPLE_RATE//2-1)], btype='band', fs=SAMPLE_RATE)
sib = _sig.lfilter(bl, al, numpy.pad(sib, (0, max(0, n_samples-cl))))[:cl]
sib *= numpy.exp(-numpy.linspace(0, 10, cl))
out[:cl] = sib * 0.6 + out[:cl] * 0.4
elif c in 'mn':
nl = min(int(SAMPLE_RATE * 0.06), n_samples)
nasal = numpy.sin(2*numpy.pi*250*t[:nl]) * 0.4 * numpy.exp(-numpy.linspace(0, 4, nl))
out[:nl] = nasal + out[:nl] * 0.4
elif c in 'fv':
fric = rng.uniform(-0.25, 0.25, cl) * numpy.exp(-numpy.linspace(0, 12, cl))
out[:cl] = fric * 0.5 + out[:cl] * 0.5
elif c in 'lr':
gl = min(int(SAMPLE_RATE * 0.05), n_samples)
ghz = hz * 0.7 + hz * 0.3 * numpy.linspace(0, 1, gl)
glide = numpy.sin(numpy.cumsum(2*numpy.pi*ghz/SAMPLE_RATE)) * 0.35
out[:gl] = glide + out[:gl] * 0.65
elif c == 'h':
hl = min(int(SAMPLE_RATE * 0.05), n_samples)
asp = rng.uniform(-0.4, 0.4, hl) * numpy.exp(-numpy.linspace(0, 5, hl))
out[:hl] = asp * 0.6 + out[:hl] * 0.4
elif c == 'w':
wl = min(int(SAMPLE_RATE * 0.06), n_samples)
ws = numpy.sin(numpy.cumsum(2*numpy.pi*hz/SAMPLE_RATE*numpy.ones(wl)))
if wl > 20:
bp, ap = _sig.butter(2, [max(20,300), min(800, SAMPLE_RATE//2-1)], btype='band', fs=SAMPLE_RATE)
ws = _sig.lfilter(bp, ap, ws)
ws *= numpy.linspace(0.5, 0, wl)
out[:wl] = ws * 0.4 + out[:wl] * 0.6
# Soft edges — prevent clicks at note boundaries
fade_samples = min(int(SAMPLE_RATE * 0.01), n_samples // 4)
if fade_samples > 0:
out[:fade_samples] *= numpy.linspace(0, 1, fade_samples)
out[-fade_samples:] *= numpy.linspace(1, 0, fade_samples)
mx = numpy.abs(out).max()
if mx > 0:
out /= mx
return (peak * out).astype(numpy.int16)
def granular_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE,
grain_size=0.04, density=50, scatter=0.5,
pitch_var=12, source="saw"):
@@ -987,6 +1130,359 @@ 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.
The banjo's distinctive twang comes from the membrane head
(like a drum skin) instead of a wooden soundboard. This gives
a sharp attack, bright tone, and fast decay with a nasal,
metallic quality. The 5th string drone adds shimmer.
"""
period = int(SAMPLE_RATE / hz)
if period < 2:
period = 2
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Steel string — bright, sharp attack
buf = rng.uniform(-0.9, 0.9, period).astype(numpy.float64)
# Minimal filtering — banjo keeps the brightness
for k in range(period - 1):
buf[k] = 0.7 * buf[k] + 0.3 * buf[k + 1]
out = numpy.zeros(n_samples, dtype=numpy.float64)
for i in range(n_samples):
out[i] = buf[i % period]
next_idx = (i + 1) % period
# Moderate decay — drum head rings but shorter than guitar
buf[i % period] = 0.5 * (buf[i % period] + buf[next_idx]) * 0.9988
# Drum-head resonance — nasal, ringy, mid-frequency peaks
# The membrane head rings more than wood — that's the twang
import scipy.signal as _sig
for center, bw, gain in [(600, 200, 0.5), (1500, 300, 0.4), (3000, 500, 0.25)]:
lo = max(20, center - bw)
hi = min(SAMPLE_RATE // 2 - 1, center + bw)
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
out += _sig.lfilter(bp, ap, out) * gain
mx = numpy.abs(out).max()
if mx > 0:
out /= mx
return (peak * out).astype(numpy.int16)
def mandolin_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Mandolin — paired steel strings, bright and ringing.
The mandolin has 4 courses of paired strings, tuned in unison.
The doubled strings create natural chorus. Bright attack from
the plectrum, small body with high-frequency resonance.
"""
period = int(SAMPLE_RATE / hz)
if period < 2:
period = 2
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Two strings per course — slightly detuned for natural chorus
buf1 = rng.uniform(-0.8, 0.8, period).astype(numpy.float64)
period2 = max(2, period + rng.integers(-1, 2))
buf2 = rng.uniform(-0.8, 0.8, period2).astype(numpy.float64)
# Light filtering — steel is brighter than nylon
for k in range(period - 1):
buf1[k] = 0.65 * buf1[k] + 0.35 * buf1[k + 1]
for k in range(period2 - 1):
buf2[k] = 0.65 * buf2[k] + 0.35 * buf2[k + 1]
out = numpy.zeros(n_samples, dtype=numpy.float64)
for i in range(n_samples):
s1 = buf1[i % period]
s2 = buf2[i % period2]
out[i] = s1 * 0.55 + s2 * 0.45
next1 = (i + 1) % period
buf1[i % period] = 0.5 * (s1 + buf1[next1]) * 0.9988
next2 = (i + 1) % period2
buf2[i % period2] = 0.5 * (s2 + buf2[next2]) * 0.9988
# Small bright body — higher resonance than guitar
import scipy.signal as _sig
for center, bw, gain in [(500, 120, 0.3), (1000, 200, 0.25), (2000, 300, 0.15)]:
lo = max(20, center - bw)
hi = min(SAMPLE_RATE // 2 - 1, center + bw)
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
out += _sig.lfilter(bp, ap, out) * gain
mx = numpy.abs(out).max()
if mx > 0:
out /= mx
return (peak * out).astype(numpy.int16)
def ukulele_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Ukulele — nylon strings on a small resonant body.
Brighter and thinner than guitar, shorter sustain. The small
body gives a mid-heavy resonance (no deep bass). Nylon strings
have a softer, warmer attack than steel.
"""
period = int(SAMPLE_RATE / hz)
if period < 2:
period = 2
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
# Nylon string — soft noise
buf = rng.uniform(-0.5, 0.5, period).astype(numpy.float64)
for _ in range(5):
for k in range(period - 1):
buf[k] = 0.55 * buf[k] + 0.45 * buf[k + 1]
out = numpy.zeros(n_samples, dtype=numpy.float64)
for i in range(n_samples):
out[i] = buf[i % period]
next_idx = (i + 1) % period
buf[i % period] = 0.5 * (buf[i % period] + buf[next_idx]) * 0.998
# Small body resonance — mid-heavy, no deep bass
import scipy.signal as _sig
for center, bw, gain in [(350, 100, 0.35), (700, 150, 0.25), (1200, 200, 0.15)]:
lo = max(20, center - bw)
hi = min(SAMPLE_RATE // 2 - 1, center + bw)
if lo < hi:
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
out += _sig.lfilter(bp, ap, out) * gain
bl, al = _sig.butter(2, min(6000, hz * 12), btype='low', fs=SAMPLE_RATE)
out = _sig.lfilter(bl, al, out)
mx = numpy.abs(out).max()
if mx > 0:
out /= mx
return (peak * out).astype(numpy.int16)
def acoustic_guitar_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
"""Acoustic guitar — Karplus-Strong with wooden body resonance.
@@ -1245,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):
@@ -1290,6 +1789,17 @@ class Synth(Enum):
TIMPANI = "timpani_synth"
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"
ACOUSTIC_GUITAR = "acoustic_guitar_synth"
SITAR = "sitar_synth"
ELECTRIC_GUITAR = "electric_guitar_synth"
@@ -1312,7 +1822,13 @@ _SYNTH_FUNCTIONS = {
"harpsichord_synth": harpsichord_wave, "cello_synth": cello_wave,
"harp_synth": harp_wave, "upright_bass_synth": upright_bass_wave,
"timpani_synth": timpani_wave, "saxophone_synth": saxophone_wave,
"granular_synth": granular_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,
"sitar_synth": sitar_wave, "electric_guitar_synth": electric_guitar_wave,
}
@@ -2068,6 +2584,68 @@ def _synth_mridangam_tha(n_samples):
return out
def _synth_cajon_bass(n_samples):
"""Cajón bass — palm strike on center of the face.
Deep woody thump. The box resonates like a bass drum but with
a warmer, more wooden character.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Wooden box thump
thump_len = min(int(SAMPLE_RATE * 0.06), n_samples)
thump_raw = _noise(thump_len)
import scipy.signal as _sig
if thump_len > 20:
bl, al = _sig.butter(2, [40, 200], btype='band', fs=SAMPLE_RATE)
thump = _sig.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len].astype(numpy.float32)
else:
thump = thump_raw
thump *= _exp_decay(thump_len, 18) * 0.8
body = numpy.sin(2 * numpy.pi * 70 * t) * _exp_decay(n_samples, 7) * 0.8
sub = _sine_f32(45, n_samples) * _exp_decay(n_samples, 9) * 0.4
click_len = min(200, n_samples)
click = _noise(click_len) * _exp_decay(click_len, 45) * 0.3
result = body + sub
result[:thump_len] += thump
result[:click_len] += click
return numpy.tanh(result * 1.3).astype(numpy.float32)
def _synth_cajon_slap(n_samples):
"""Cajón slap — fingers near the top edge, snare wires buzz.
Bright crack with a buzzy rattle from the internal snare wires.
The signature cajón sound — like a snare but woodier.
"""
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
# Snare wire buzz
wire = _noise(n_samples) * _exp_decay(n_samples, 18) * 0.6
import scipy.signal as _sig
bl, al = _sig.butter(2, [1500, 6000], btype='band', fs=SAMPLE_RATE)
wire = _sig.lfilter(bl, al, wire).astype(numpy.float32) * 1.2
# Wood body
body = numpy.sin(2 * numpy.pi * 200 * t) * _exp_decay(n_samples, 22) * 0.4
# Sharp slap
slap_len = min(int(SAMPLE_RATE * 0.008), n_samples)
slap = _noise(slap_len) * _exp_decay(slap_len, 200) * 0.8
result = body + wire
result[:slap_len] += slap
return numpy.tanh(result * 1.5).astype(numpy.float32)
def _synth_cajon_tap(n_samples):
"""Cajón tap — light fingertip on the face. Ghost note."""
n = min(n_samples, int(SAMPLE_RATE * 0.04))
t = numpy.arange(n, dtype=numpy.float32) / SAMPLE_RATE
tap = numpy.sin(2 * numpy.pi * 300 * t) * _exp_decay(n, 35) * 0.3
pop = _noise(min(50, n)) * _exp_decay(min(50, n), 250) * 0.5
result = tap
result[:min(50, n)] += pop
out = numpy.zeros(n_samples, dtype=numpy.float32)
out[:n] = numpy.tanh(result * 1.5)
return out
def _synth_metal_kick(n_samples):
"""Metal kick — punchy with beater click. Double-bass ready.
@@ -2336,6 +2914,10 @@ def _render_drum_hit(sound_value, n_samples):
DrumSound.DJEMBE_BASS.value: lambda n: _synth_djembe_bass(n),
DrumSound.DJEMBE_TONE.value: lambda n: _synth_djembe_tone(n),
DrumSound.DJEMBE_SLAP.value: lambda n: _synth_djembe_slap(n),
# Cajon
DrumSound.CAJON_BASS.value: lambda n: _synth_cajon_bass(n),
DrumSound.CAJON_SLAP.value: lambda n: _synth_cajon_slap(n),
DrumSound.CAJON_TAP.value: lambda n: _synth_cajon_tap(n),
# Metal kit
DrumSound.METAL_KICK.value: lambda n: _synth_metal_kick(n),
DrumSound.METAL_SNARE.value: lambda n: _synth_metal_snare(n),
@@ -3528,8 +4110,13 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
bent = src_f[idx] * (1 - frac) + src_f[numpy.minimum(idx + 1, src_len - 1)] * frac
waves.append((bent * SAMPLE_PEAK).astype(numpy.int16))
else:
# Render oscillators (pass synth_kwargs for FM etc.)
waves = [synth_fn(hz, n_samples=n_samples, **_skw)
# Per-note kwargs (e.g. lyric for vocal synth)
note_skw = dict(_skw)
note_lyric = getattr(note, 'lyric', '')
if note_lyric:
note_skw['lyric'] = note_lyric
# Render oscillators
waves = [synth_fn(hz, n_samples=n_samples, **note_skw)
for hz in pitches]
# Sub-oscillator: octave-below sine
if sub_osc > 0:
@@ -3920,6 +4507,10 @@ def render_score(score):
DrumSound.DJEMBE_BASS.value: 0.0,
DrumSound.DJEMBE_TONE.value: 0.1,
DrumSound.DJEMBE_SLAP.value: -0.1,
# Cajon — centered (single instrument)
DrumSound.CAJON_BASS.value: 0.0,
DrumSound.CAJON_SLAP.value: 0.0,
DrumSound.CAJON_TAP.value: 0.1,
# Metal kit
DrumSound.METAL_KICK.value: 0.0,
DrumSound.METAL_SNARE.value: 0.0,
@@ -4046,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)]
+125 -4
View File
@@ -195,6 +195,54 @@ 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,
},
"mandolin": {
"synth": "mandolin_synth", "envelope": "none",
"humanize": 0.2,
},
"mandola": {
"synth": "mandolin_synth", "envelope": "none",
"lowpass": 3000,
"humanize": 0.2,
},
"ukulele": {
"synth": "ukulele_synth", "envelope": "none",
"humanize": 0.2,
},
"koto": {
"synth": "pluck_synth", "envelope": "none",
"lowpass": 4000,
@@ -246,6 +294,16 @@ INSTRUMENTS = {
"reverb": 0.4, "reverb_type": "cathedral",
"analog": 0.3,
},
"vocal": {
"synth": "vocal_synth", "envelope": "strings",
"reverb": 0.3, "reverb_type": "hall",
"humanize": 0.15,
},
"choir": {
"synth": "vocal_synth", "envelope": "pad",
"detune": 8, "spread": 0.4,
"reverb": 0.45, "reverb_type": "cathedral",
},
"granular_texture": {
"synth": "granular_synth", "envelope": "none",
"reverb": 0.5, "reverb_type": "taj_mahal",
@@ -367,6 +425,7 @@ class Note:
velocity: int = 100
bend: float = 0.0
bend_type: str = "smooth" # "smooth" (log), "linear", "late"
lyric: str = "" # syllable for vocal synth
@property
def beats(self) -> float:
@@ -468,6 +527,10 @@ class DrumSound(Enum):
DJEMBE_BASS = 102 # open bass (center of head)
DJEMBE_TONE = 103 # open tone (edge, fingers together)
DJEMBE_SLAP = 104 # slap (edge, fingers spread, sharp crack)
# Cajon sounds
CAJON_BASS = 108 # center of face, deep thump
CAJON_SLAP = 109 # top edge, snare wires buzz
CAJON_TAP = 110 # light finger tap
# Metal kit — tighter, punchier, more attack
METAL_KICK = 105 # clicky, punchy, tight
METAL_SNARE = 106 # crack, bright, cutting
@@ -1499,6 +1562,50 @@ Pattern._PRESETS["tabla solo"] = dict(
],
)
# ── Cajón patterns ────────────────────────────────────────────────────────
CB = DrumSound.CAJON_BASS
CSL = DrumSound.CAJON_SLAP
CT = DrumSound.CAJON_TAP
# Cajón flamenco — the classic acoustic percussion groove
Pattern._PRESETS["cajon"] = dict(
name="cajon",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 85), _h(CT, 0.5, 35), _h(CT, 0.75, 38),
_h(CSL, 1.0, 80), _h(CT, 1.5, 32),
_h(CB, 2.0, 82), _h(CT, 2.5, 35), _h(CT, 2.75, 40),
_h(CSL, 3.0, 82), _h(CT, 3.25, 30), _h(CT, 3.5, 35),
],
)
# Cajón rumba — Latin-flavored
Pattern._PRESETS["cajon rumba"] = dict(
name="cajon rumba",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 88), _h(CT, 0.5, 38),
_h(CSL, 1.0, 78), _h(CT, 1.25, 32), _h(CB, 1.5, 72),
_h(CSL, 2.0, 82), _h(CT, 2.5, 35),
_h(CB, 3.0, 75), _h(CSL, 3.5, 80), _h(CT, 3.75, 38),
],
)
# Cajón singer-songwriter — simple, supportive
Pattern._PRESETS["cajon folk"] = dict(
name="cajon folk",
time_signature="4/4",
beats=4.0,
hits=[
_h(CB, 0.0, 80),
_h(CSL, 1.0, 72), _h(CT, 1.5, 30),
_h(CB, 2.0, 78),
_h(CSL, 3.0, 75),
],
)
# ── Metal kit patterns ────────────────────────────────────────────────────
MK = DrumSound.METAL_KICK
MS = DrumSound.METAL_SNARE
@@ -2095,7 +2202,7 @@ class Part:
self._automation: list[tuple[float, dict]] = [] # (beat, {param: value})
def add(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
bend: float = 0.0, bend_type: str = "smooth") -> "Part":
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "") -> "Part":
"""Add a note. Accepts Tone/Chord objects or note strings like ``"E5"``.
Duration can be a ``Duration`` enum or a raw float (beats).
@@ -2113,7 +2220,7 @@ class Part:
duration = _RawDuration(duration)
self.notes.append(Note(tone=tone_or_string, duration=duration,
velocity=velocity, bend=bend,
bend_type=bend_type))
bend_type=bend_type, lyric=lyric))
return self
def set(self, **params) -> "Part":
@@ -2398,7 +2505,7 @@ class Part:
def strum(self, chord_name: str, duration=Duration.QUARTER, *,
direction: str = "down", velocity: int = 100,
strum_time: float = 0.08) -> "Part":
strum_time: float = 0.05) -> "Part":
"""Strum a chord using the part's fretboard fingering.
Looks up the chord on the fretboard, gets the fingering, and
@@ -2466,7 +2573,21 @@ class Part:
from .chords import Chord as ChordClass
chord_obj = ChordClass(tones=strum_tones)
self.add(chord_obj, total_beats, velocity=velocity)
# Strum sweep: quick individual string hits before the chord.
# Only the first 2-3 strings get a tiny grace note, the rest
# ring together as the full chord. Gives the strum feel without
# sounding like separate plucks.
n_strings = len(strum_tones)
if strum_time > 0.02 and n_strings >= 3:
n_grace = min(2, n_strings - 1)
per_grace = strum_time / n_grace
grace_vel = max(1, int(velocity * 0.25))
for i in range(n_grace):
self.add(strum_tones[i], per_grace, velocity=grace_vel)
ring = max(0.1, total_beats - strum_time)
self.add(chord_obj, ring, velocity=velocity)
else:
self.add(chord_obj, total_beats, velocity=velocity)
return self
+241 -2
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
@@ -6827,7 +6827,7 @@ def test_strum_direction():
p = score.part("g", instrument="acoustic_guitar", fretboard=fb)
p.strum("G", Duration.QUARTER, direction="down")
p.strum("G", Duration.QUARTER, direction="up")
assert len(p.notes) == 2
assert len(p.notes) >= 2 # grace notes + chord per strum
# ── World drums ──────────────────────────────────────────────────────────────
@@ -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.35.1"
version = "0.36.2"
source = { editable = "." }
dependencies = [
{ name = "numeral" },