mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8d1fe5e81 | |||
| 2612444146 | |||
| 48a954d063 | |||
| 8a07be23e6 | |||
| d771117d5c | |||
| 1ae9404f07 | |||
| 2e5b18de2e | |||
| 248594fb21 | |||
| 3ce890c54c | |||
| 499c49b6eb | |||
| f85504b456 | |||
| d0624f8b78 | |||
| fb37a7c27b | |||
| fb36e75a42 | |||
| 81b54d2394 | |||
| fa6a3090cb | |||
| 58286ddb69 | |||
| 6d137be9f5 | |||
| 383802a1e1 | |||
| 7f2aeb2395 | |||
| 16b4c7d1fa | |||
| b9ee5c9cde | |||
| 7375d58209 | |||
| 6316a6c910 | |||
| 237cfe171c | |||
| 1751f97617 | |||
| 943a12b3bb | |||
| 04d2de3e70 | |||
| ead42751ef | |||
| 8dee0d00d8 | |||
| de112e0d9f | |||
| f469ad90f8 | |||
| bab7f39304 | |||
| 62557ba534 | |||
| 8b50a9c325 | |||
| 7e9caac70b | |||
| a0756b3172 | |||
| e3dd706032 | |||
| 9b412906bc | |||
| 54e0421997 | |||
| 109343ad30 | |||
| 28e84de566 | |||
| d353d64298 | |||
| 7ee02e7ed2 | |||
| a5c9a46eb2 | |||
| f9c63ec360 | |||
| b9e88b77d8 | |||
| 1910b09132 | |||
| 0c5287450b | |||
| 5ac1873d83 | |||
| 9fafca2b08 | |||
| af044f68ca | |||
| 60f697f846 |
@@ -2,6 +2,28 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.40.0
|
||||
|
||||
- **Rhodes electric piano synth** — tine + tonebar + electromagnetic
|
||||
pickup model. `electric_piano` preset now uses dedicated `rhodes_synth`
|
||||
instead of FM
|
||||
- **73 audio demos in docs** — every synth, every drum pattern, every
|
||||
code example with `play_score()` now has an embedded audio player
|
||||
- Idiomatic demos: harp arpeggiates, guitars strum, cello bows, sitar
|
||||
drones, strings use ensemble
|
||||
- Trailing silence trimming on all audio exports
|
||||
- Raw waveform demos (no envelope) for classic waveforms
|
||||
|
||||
## 0.39.3
|
||||
|
||||
- **33 audio samples in documentation** — every `play_score()` example
|
||||
now has an embedded stereo audio player. Covers quickstart, sequencing,
|
||||
drums (all world percussion), playback, and cookbook.
|
||||
- **`docs/generate_audio.py`** — renders all doc examples to WAV
|
||||
- Numpy vectorization: cached time arrays, decay envelopes, drum hits;
|
||||
vectorized piano harmonic synthesis
|
||||
- Fixed acid legato example (removed pad envelope, added proper 303 recipe)
|
||||
|
||||
## 0.39.2
|
||||
|
||||
- **Marching percussion** — snare, rimshot, and stick click sounds with
|
||||
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
<audio controls style="width: 100%; margin: 0.5em 0 1.5em 0;">
|
||||
<source src="{{ pathto('_static/audio/' + file, 1) }}" type="audio/wav">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -411,6 +411,10 @@ Acid House Track
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/acid_house.wav" type="audio/wav"></audio>
|
||||
|
||||
Dub Reggae with Delay Madness
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -443,6 +447,10 @@ Sparse notes into infinite echo:
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dub_reggae.wav" type="audio/wav"></audio>
|
||||
|
||||
Jazz Ballad with Humanize
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -480,6 +488,10 @@ The difference between a robot and a musician:
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/jazz_ballad.wav" type="audio/wav"></audio>
|
||||
|
||||
Song with Sections
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -513,6 +525,10 @@ Define once, arrange freely:
|
||||
play_score(score)
|
||||
score.save_midi("my_song.mid")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/song_sections.wav" type="audio/wav"></audio>
|
||||
|
||||
Export Everything to MIDI
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -249,6 +249,13 @@ Playing Patterns
|
||||
play_pattern(Pattern.preset("salsa"), repeats=4, bpm=180)
|
||||
play_pattern(Pattern.preset("afrobeat"), repeats=8, bpm=110)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/rock_beat.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/bossa_nova_pattern.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/salsa_pattern.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 1.5em"><source src="../_static/audio/afrobeat_pattern.wav" type="audio/wav"></audio>
|
||||
|
||||
Fills
|
||||
-----
|
||||
|
||||
@@ -339,6 +346,10 @@ drum pattern and all named parts are mixed together by ``play_score()``:
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/salsa_layered.wav" type="audio/wav"></audio>
|
||||
|
||||
World Percussion
|
||||
----------------
|
||||
|
||||
@@ -382,6 +393,12 @@ bayan (deep bass bends showcase), tabla call (dayan/bayan call-and-response).
|
||||
score = Score("4/4", bpm=80)
|
||||
score.drums("teental", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/tabla_teental.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/tabla_keherwa.wav" type="audio/wav"></audio>
|
||||
<audio controls style="width:100%;margin:0.3em 0 1.5em"><source src="../_static/audio/tabla_chakradar.wav" type="audio/wav"></audio>
|
||||
|
||||
Dhol
|
||||
~~~~
|
||||
|
||||
@@ -399,6 +416,10 @@ energetic, and physically impossible to sit still to.
|
||||
score = Score("4/4", bpm=160)
|
||||
score.drums("bhangra", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dhol.wav" type="audio/wav"></audio>
|
||||
|
||||
Dholak
|
||||
~~~~~~
|
||||
|
||||
@@ -416,6 +437,10 @@ music) and dholak folk (a general folk groove).
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("qawwali", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dholak.wav" type="audio/wav"></audio>
|
||||
|
||||
Mridangam
|
||||
~~~~~~~~~
|
||||
|
||||
@@ -434,6 +459,10 @@ and mridangam korvai (a rhythmic cadence pattern).
|
||||
score = Score("4/4", bpm=90)
|
||||
score.drums("adi talam", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/mridangam.wav" type="audio/wav"></audio>
|
||||
|
||||
Djembe
|
||||
~~~~~~
|
||||
|
||||
@@ -458,6 +487,10 @@ West African-style break).
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("djembe", repeats=8, fill="djembe call", fill_every=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/djembe.wav" type="audio/wav"></audio>
|
||||
|
||||
Metal Kit
|
||||
~~~~~~~~~
|
||||
|
||||
@@ -482,6 +515,10 @@ roll → kick roll → alternating → crash ending).
|
||||
score = Score("4/4", bpm=200)
|
||||
score.drums("metal blast", repeats=8, fill="metal cascade", fill_every=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/metal_blast.wav" type="audio/wav"></audio>
|
||||
|
||||
Cajón
|
||||
~~~~~
|
||||
|
||||
@@ -504,6 +541,10 @@ bass-slap groove).
|
||||
score = Score("4/4", bpm=100)
|
||||
score.drums("cajon", repeats=8, fill="cajon flam", fill_every=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/cajon.wav" type="audio/wav"></audio>
|
||||
|
||||
Marching Percussion
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -548,6 +589,10 @@ voice with per-player timing tendencies and micro pitch drift.
|
||||
# Or use patterns
|
||||
score.drums("drumline", repeats=4)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/march_snare.wav" type="audio/wav"></audio>
|
||||
|
||||
**Sympathetic resonance:** The marching snare builds up snare wire
|
||||
buzz as hits accumulate, and the buzz decays during rests — just like
|
||||
a real drum.
|
||||
|
||||
@@ -66,6 +66,10 @@ the mix louder and punchier:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/playback_basic.wav" type="audio/wav"></audio>
|
||||
|
||||
The render pipeline respects the Score's ``temperament`` and
|
||||
``reference_pitch`` settings, so Baroque or microtonal scores play back
|
||||
at the correct tuning:
|
||||
|
||||
+19
-33
@@ -143,48 +143,34 @@ chords, melody, bass, each with their own synth and effects:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Pattern, Key, Duration, Chord
|
||||
from pytheory import Score, Key, Duration
|
||||
from pytheory.play import play_score
|
||||
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=8, fill="rock", fill_every=4)
|
||||
|
||||
chords = score.part(
|
||||
"chords",
|
||||
synth="fm",
|
||||
envelope="pad",
|
||||
reverb=0.4,
|
||||
)
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="saw",
|
||||
envelope="pluck",
|
||||
delay=0.3,
|
||||
lowpass=3000,
|
||||
humanize=0.2,
|
||||
)
|
||||
bass = score.part(
|
||||
"bass",
|
||||
synth="sine",
|
||||
lowpass=500,
|
||||
)
|
||||
piano = score.part("piano", instrument="piano", reverb=0.3)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck",
|
||||
delay=0.2, reverb=0.2, lowpass=4000)
|
||||
bass = score.part("bass", synth="triangle", lowpass=900)
|
||||
|
||||
key = Key("A", "minor")
|
||||
for chord in key.progression("i", "iv", "V", "i"):
|
||||
chords.add(chord, Duration.WHOLE)
|
||||
chords.add(chord, Duration.WHOLE)
|
||||
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
|
||||
piano.add(chord, Duration.WHOLE)
|
||||
|
||||
lead.arpeggio("Am", bars=2, pattern="updown", octaves=2)
|
||||
lead.arpeggio("Dm", bars=2, pattern="updown", octaves=2)
|
||||
lead.set(lowpass=5000, reverb=0.3)
|
||||
lead.arpeggio("E7", bars=2, pattern="up", octaves=2)
|
||||
lead.arpeggio("Am", bars=2, pattern="updown", octaves=2)
|
||||
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
|
||||
lead.add("G5", 1).add("E5", 1)
|
||||
lead.add("D5", 0.5).add("B4", 0.5).add("A4", 1)
|
||||
lead.add("G4", 2).rest(2)
|
||||
|
||||
for n in ["A2", "E2", "A2", "C3"] * 4:
|
||||
bass.add(n, Duration.QUARTER)
|
||||
for n in ["G2", "G2", "D2", "D2", "E2", "E2", "C2", "C2"] * 2:
|
||||
bass.add(n, Duration.HALF)
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/quickstart.wav" type="audio/wav"></audio>
|
||||
|
||||
Export to Your DAW
|
||||
------------------
|
||||
|
||||
|
||||
+86
-41
@@ -161,6 +161,10 @@ Chords work just like tones — pass any ``Chord`` object:
|
||||
for chord in chords:
|
||||
score.add(chord, Duration.WHOLE)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/chords_basic.wav" type="audio/wav"></audio>
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> score.measures
|
||||
@@ -244,6 +248,31 @@ Chords and Tone objects work the same way:
|
||||
for note in ["A2", "C3", "E3", "A2", "D2", "F2", "A2", "D2"]:
|
||||
bass.add(note, Duration.QUARTER)
|
||||
|
||||
Polyphonic Hold
|
||||
---------------
|
||||
|
||||
``Part.hold()`` adds a note without advancing the beat position —
|
||||
the next note starts at the *same* time. This enables polyphonic
|
||||
overlap on a single part: piano sustain, sitar drone under melody,
|
||||
guitar strum texture.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
piano = score.part("piano", instrument="piano", reverb=0.3)
|
||||
|
||||
# Hold a C major chord for 8 beats
|
||||
piano.hold("C3", Duration.WHOLE * 2, velocity=60)
|
||||
piano.hold("E3", Duration.WHOLE * 2, velocity=55)
|
||||
piano.hold("G3", Duration.WHOLE * 2, velocity=55)
|
||||
|
||||
# Melody plays simultaneously on top
|
||||
for n in ["E4", "G4", "C5", "G4", "E4", "D4", "C4", "E4"]:
|
||||
piano.add(n, Duration.QUARTER, velocity=80)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/piano_hold.wav" type="audio/wav"></audio>
|
||||
|
||||
Arpeggiator
|
||||
------------
|
||||
|
||||
@@ -292,6 +321,10 @@ Chain arpeggios through a progression:
|
||||
for sym in ["Cm", "Fm", "Abm", "Gm"]:
|
||||
lead.arpeggio(sym, bars=2, pattern="updown", octaves=2)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/arpeggio.wav" type="audio/wav"></audio>
|
||||
|
||||
Combined with legato, glide, distortion, and a resonant lowpass, this
|
||||
produces the classic acid/trance arpeggiator sound.
|
||||
|
||||
@@ -322,12 +355,18 @@ portamento (pitch slides between notes):
|
||||
acid = score.part(
|
||||
"acid",
|
||||
synth="saw",
|
||||
envelope="pad",
|
||||
legato=True,
|
||||
glide=0.04,
|
||||
lowpass=3000,
|
||||
lowpass_q=6.0,
|
||||
distortion=0.3,
|
||||
)
|
||||
acid.add("C2", 0.25).add("C3", 0.25).add("G2", 0.25).add("C2", 0.25)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/legato_glide.wav" type="audio/wav"></audio>
|
||||
|
||||
- ``legato``: If True, no envelope retrigger between notes (default False).
|
||||
- ``glide``: Portamento time in seconds (default 0, instant).
|
||||
0.03--0.05 = quick 303 slide, 0.1--0.2 = slow glide.
|
||||
@@ -335,63 +374,51 @@ portamento (pitch slides between notes):
|
||||
Complete Example
|
||||
----------------
|
||||
|
||||
A full multi-part arrangement built from scratch — bossa nova with FM
|
||||
rhodes, triangle lead, and filtered bass:
|
||||
A full multi-part arrangement — rock beat with piano chords, saw
|
||||
lead, and filtered bass:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Pattern, Key, Duration, Chord
|
||||
from pytheory import Score, Key, Duration, Chord
|
||||
from pytheory.play import play_score
|
||||
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=8, fill="rock", fill_every=4)
|
||||
|
||||
# FM rhodes with reverb
|
||||
rhodes = score.part(
|
||||
"rhodes",
|
||||
synth="fm",
|
||||
envelope="piano",
|
||||
volume=0.3,
|
||||
reverb=0.4,
|
||||
reverb_decay=1.8,
|
||||
)
|
||||
# Piano chords with reverb
|
||||
piano = score.part("piano", instrument="piano", volume=0.4, reverb=0.3)
|
||||
|
||||
# Triangle lead with delay
|
||||
# Saw lead with delay
|
||||
lead = score.part(
|
||||
"lead",
|
||||
synth="triangle",
|
||||
envelope="pluck",
|
||||
volume=0.45,
|
||||
delay=0.25,
|
||||
delay_time=0.32,
|
||||
delay_feedback=0.35,
|
||||
reverb=0.2,
|
||||
"lead", synth="saw", envelope="pluck", volume=0.4,
|
||||
delay=0.2, delay_time=0.33, reverb=0.2, lowpass=3000,
|
||||
)
|
||||
|
||||
# Filtered bass
|
||||
bass = score.part(
|
||||
"bass",
|
||||
synth="sine",
|
||||
envelope="pluck",
|
||||
volume=0.45,
|
||||
lowpass=600,
|
||||
)
|
||||
bass = score.part("bass", synth="triangle", envelope="pluck",
|
||||
volume=0.45, lowpass=1200)
|
||||
|
||||
for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
|
||||
rhodes.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
|
||||
piano.add(chord, Duration.WHOLE)
|
||||
|
||||
for n, d in [
|
||||
("E5", 0.67), ("D5", 0.33), ("C5", 0.67), ("B4", 0.33),
|
||||
("A4", 1), ("C5", 0.67), ("E5", 0.33), ("D5", 0.67), ("C5", 0.33),
|
||||
("A4", 1),
|
||||
]:
|
||||
lead.add(n, d)
|
||||
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
|
||||
lead.add("G5", 1).add("E5", 1)
|
||||
lead.add("D5", 0.5).add("B4", 0.5).add("A4", 1)
|
||||
lead.add("G4", 2).rest(2)
|
||||
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
|
||||
lead.add("G5", 1).add("A5", 1)
|
||||
lead.add("G5", 0.5).add("E5", 0.5).add("D5", 1)
|
||||
lead.add("B4", 2).rest(2)
|
||||
|
||||
for n in ["A2", "E2", "A2", "C3", "D2", "A2", "D2", "F2"]:
|
||||
bass.add(n, Duration.QUARTER)
|
||||
for n in ["G2", "G2", "D2", "D2", "E2", "E2", "C2", "C2"] * 2:
|
||||
bass.add(n, Duration.HALF)
|
||||
|
||||
play_score(score)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/complete_rock.wav" type="audio/wav"></audio>
|
||||
|
||||
Velocity
|
||||
--------
|
||||
|
||||
@@ -431,6 +458,10 @@ Pass ``articulation=`` to ``Part.add()``:
|
||||
piano.add("G4", Duration.QUARTER, articulation="accent") # louder
|
||||
piano.add("C5", Duration.HALF, articulation="fermata") # held longer
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/articulations.wav" type="audio/wav"></audio>
|
||||
|
||||
What each articulation does:
|
||||
|
||||
- **staccato** — plays ~40% of the note duration with a quick fade-out. Short and detached.
|
||||
@@ -467,6 +498,10 @@ of notes instead of setting each one manually.
|
||||
piano.dynamics(["C4","E4","G4","C5"], Duration.QUARTER,
|
||||
velocities=[50, 80, 110, 90])
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/dynamics.wav" type="audio/wav"></audio>
|
||||
|
||||
Four methods:
|
||||
|
||||
- **crescendo()** — linear velocity ramp from ``start_vel`` to ``end_vel``.
|
||||
@@ -548,6 +583,12 @@ Each ensemble voice gets a consistent timing personality (some rush,
|
||||
some drag) plus small per-note wobble, and slightly different tuning.
|
||||
The result sounds like a real section — together but alive.
|
||||
|
||||
Solo snare, then an 8-player section plays the same pattern:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/ensemble.wav" type="audio/wav"></audio>
|
||||
|
||||
Swing and Groove
|
||||
----------------
|
||||
|
||||
@@ -663,6 +704,10 @@ Four interpolation curves:
|
||||
# Smooth reverb wash fading in and settling
|
||||
pad.ramp(over=Duration.WHOLE * 4, curve="ease_in_out", reverb=0.6)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="../_static/audio/filter_ramp.wav" type="audio/wav"></audio>
|
||||
|
||||
``ramp()`` generates automation points every quarter-beat by default.
|
||||
Set ``resolution=0.125`` for smoother curves (every 32nd note), or
|
||||
``resolution=1.0`` for lighter automation (every beat).
|
||||
|
||||
+180
-4
@@ -37,6 +37,10 @@ building block of all other waveforms (Fourier's theorem).
|
||||
tone = Tone.from_string("C4", system="western")
|
||||
play(tone, synth=Synth.SINE)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_sine.wav" type="audio/wav"></audio>
|
||||
|
||||
Sawtooth
|
||||
~~~~~~~~
|
||||
|
||||
@@ -50,6 +54,10 @@ Named for its ramp shape.
|
||||
|
||||
play(tone, synth=Synth.SAW)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_saw.wav" type="audio/wav"></audio>
|
||||
|
||||
Triangle
|
||||
~~~~~~~~
|
||||
|
||||
@@ -63,6 +71,10 @@ described as "woody" or "hollow."
|
||||
|
||||
play(tone, synth=Synth.TRIANGLE)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_triangle.wav" type="audio/wav"></audio>
|
||||
|
||||
Square
|
||||
~~~~~~
|
||||
|
||||
@@ -76,6 +88,10 @@ pulse wave with a 50% duty cycle.
|
||||
|
||||
play(tone, synth=Synth.SQUARE)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_square.wav" type="audio/wav"></audio>
|
||||
|
||||
Extended Waveforms
|
||||
------------------
|
||||
|
||||
@@ -98,6 +114,10 @@ the classic NES-style buzzy tone.
|
||||
|
||||
lead = score.part("lead", synth="pulse", envelope="pluck")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_pulse.wav" type="audio/wav"></audio>
|
||||
|
||||
FM Synthesis
|
||||
~~~~~~~~~~~~
|
||||
|
||||
@@ -109,18 +129,24 @@ the electric piano in every Whitney Houston ballad, the bass in every
|
||||
Depeche Mode track, the bells in a thousand TV jingles. If you heard
|
||||
pop music in the 80s, you heard FM synthesis.
|
||||
|
||||
**Use for:** electric piano (rhodes), bells, metallic leads, jazz chords.
|
||||
**Use for:** bells, metallic leads, glassy pads, DX7-style sounds.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
rhodes = score.part(
|
||||
"rhodes",
|
||||
bells = score.part(
|
||||
"bells",
|
||||
synth="fm",
|
||||
envelope="piano",
|
||||
envelope="bell",
|
||||
fm_ratio=3.0,
|
||||
fm_index=5.0,
|
||||
volume=0.3,
|
||||
reverb=0.4,
|
||||
)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_fm.wav" type="audio/wav"></audio>
|
||||
|
||||
Noise
|
||||
-----
|
||||
|
||||
@@ -142,6 +168,10 @@ Useful as a texture layer, a percussion source, or a wind/ocean effect.
|
||||
lowpass=2000,
|
||||
)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_noise.wav" type="audio/wav"></audio>
|
||||
|
||||
Ensemble Waveforms
|
||||
------------------
|
||||
|
||||
@@ -174,6 +204,10 @@ supersaw.
|
||||
reverb=0.5,
|
||||
)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_supersaw.wav" type="audio/wav"></audio>
|
||||
|
||||
PWM Slow
|
||||
~~~~~~~~
|
||||
|
||||
@@ -195,6 +229,10 @@ from Boards of Canada to Drake? PWM with a slow LFO.
|
||||
reverb=0.4,
|
||||
)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_pwm_slow.wav" type="audio/wav"></audio>
|
||||
|
||||
PWM Fast
|
||||
~~~~~~~~
|
||||
|
||||
@@ -207,6 +245,10 @@ produces a natural chorus/vibrato effect built into the waveform itself.
|
||||
|
||||
lead = score.part("lead", synth="pwm_fast", envelope="pluck", volume=0.5)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_pwm_fast.wav" type="audio/wav"></audio>
|
||||
|
||||
ADSR Envelopes
|
||||
--------------
|
||||
|
||||
@@ -375,6 +417,10 @@ at musical levels. Warm, round, unmistakably organ.
|
||||
|
||||
organ = score.part("organ", synth="organ_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_organ.wav" type="audio/wav"></audio>
|
||||
|
||||
String Ensemble
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -386,6 +432,10 @@ more "wooden" than a raw saw wave.
|
||||
|
||||
violin = score.part("violin", synth="strings_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_strings.wav" type="audio/wav"></audio>
|
||||
|
||||
Dedicated Instrument Synths
|
||||
--------------------------
|
||||
|
||||
@@ -407,6 +457,28 @@ soundboard.
|
||||
|
||||
piano = score.part("piano", synth="piano_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_piano.wav" type="audio/wav"></audio>
|
||||
|
||||
Rhodes Electric Piano
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Fender Rhodes — a rubber-tipped hammer strikes a steel tine
|
||||
next to a tonebar, picked up by an electromagnetic pickup. Warm,
|
||||
bell-like, with a bright metallic attack that mellows into a
|
||||
singing sustain. The sound of jazz clubs, soul, and neo-soul.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
rhodes = score.part("rhodes", synth="rhodes_synth")
|
||||
# Or use the instrument preset (adds tremolo + chorus)
|
||||
rhodes = score.part("rhodes", instrument="electric_piano")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_rhodes.wav" type="audio/wav"></audio>
|
||||
|
||||
Bass Guitar Synth
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -416,6 +488,10 @@ Plucked string model with finger-damped harmonics and low-end warmth.
|
||||
|
||||
bass = score.part("bass", synth="bass_guitar_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_bass_guitar.wav" type="audio/wav"></audio>
|
||||
|
||||
Flute Synth
|
||||
~~~~~~~~~~~~
|
||||
|
||||
@@ -426,6 +502,10 @@ overblowing behavior at higher velocities.
|
||||
|
||||
flute = score.part("flute", synth="flute_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_flute.wav" type="audio/wav"></audio>
|
||||
|
||||
Trumpet Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -436,6 +516,10 @@ velocity, plus a characteristic brassy edge from shaped harmonics.
|
||||
|
||||
trumpet = score.part("trumpet", synth="trumpet_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_trumpet.wav" type="audio/wav"></audio>
|
||||
|
||||
Clarinet Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -446,6 +530,10 @@ characteristic hollow, woody tone.
|
||||
|
||||
clarinet = score.part("clarinet", synth="clarinet_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_clarinet.wav" type="audio/wav"></audio>
|
||||
|
||||
Oboe Synth
|
||||
~~~~~~~~~~~
|
||||
|
||||
@@ -456,6 +544,10 @@ timbre.
|
||||
|
||||
oboe = score.part("oboe", synth="oboe_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_oboe.wav" type="audio/wav"></audio>
|
||||
|
||||
Marimba Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -466,6 +558,10 @@ that emphasizes the fundamental.
|
||||
|
||||
marimba = score.part("marimba", synth="marimba_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_marimba.wav" type="audio/wav"></audio>
|
||||
|
||||
Harpsichord Synth
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -476,6 +572,10 @@ Plucked-string model with a bright, immediate attack and rapid decay
|
||||
|
||||
harpsi = score.part("harpsi", synth="harpsichord_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_harpsichord.wav" type="audio/wav"></audio>
|
||||
|
||||
Cello Synth
|
||||
~~~~~~~~~~~
|
||||
|
||||
@@ -486,6 +586,10 @@ producing a rich, warm, sustained tone.
|
||||
|
||||
cello = score.part("cello", synth="cello_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_cello.wav" type="audio/wav"></audio>
|
||||
|
||||
Harp Synth
|
||||
~~~~~~~~~~
|
||||
|
||||
@@ -496,6 +600,10 @@ modeling nylon strings on a resonant frame.
|
||||
|
||||
harp = score.part("harp", synth="harp_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_harp.wav" type="audio/wav"></audio>
|
||||
|
||||
Upright Bass Synth
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -505,6 +613,10 @@ Pizzicato double bass with woody body resonance and a thumpy low end.
|
||||
|
||||
bass = score.part("bass", synth="upright_bass_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_upright_bass.wav" type="audio/wav"></audio>
|
||||
|
||||
Acoustic Guitar Synth
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -515,6 +627,10 @@ string decay.
|
||||
|
||||
guitar = score.part("guitar", synth="acoustic_guitar_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_acoustic_guitar.wav" type="audio/wav"></audio>
|
||||
|
||||
Electric Guitar Synth
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -525,6 +641,10 @@ than the acoustic, ready for effects processing.
|
||||
|
||||
eguitar = score.part("eguitar", synth="electric_guitar_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_electric_guitar.wav" type="audio/wav"></audio>
|
||||
|
||||
Sitar Synth
|
||||
~~~~~~~~~~~~
|
||||
|
||||
@@ -535,6 +655,10 @@ bridge, producing a shimmering, metallic sustain.
|
||||
|
||||
sitar = score.part("sitar", synth="sitar_synth")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_sitar.wav" type="audio/wav"></audio>
|
||||
|
||||
Timpani Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -547,6 +671,10 @@ Use ``Part.roll()`` for crescendo timpani rolls.
|
||||
timp = score.part("timp", synth="timpani_synth")
|
||||
timp.roll("C3", Duration.WHOLE, velocity_start=20, velocity_end=110)
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_timpani.wav" type="audio/wav"></audio>
|
||||
|
||||
Saxophone Synth
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -558,6 +686,10 @@ mids, reed buzz, and brass body warmth. Four presets: ``saxophone``,
|
||||
|
||||
sax = score.part("sax", instrument="tenor_sax")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_saxophone.wav" type="audio/wav"></audio>
|
||||
|
||||
Pedal Steel Synth
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -568,6 +700,10 @@ and long sustain. Pairs naturally with spring reverb.
|
||||
|
||||
steel = score.part("steel", instrument="pedal_steel")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_pedal_steel.wav" type="audio/wav"></audio>
|
||||
|
||||
Theremin Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -578,6 +714,10 @@ Best used with legato and glide for continuous pitch.
|
||||
|
||||
theremin = score.part("theremin", instrument="theremin")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_theremin.wav" type="audio/wav"></audio>
|
||||
|
||||
Kalimba Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -588,6 +728,10 @@ inharmonic overtones (modes at 1x, 2.92x, 5.4x).
|
||||
|
||||
kalimba = score.part("kalimba", instrument="kalimba")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_kalimba.wav" type="audio/wav"></audio>
|
||||
|
||||
Steel Drum Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -598,6 +742,10 @@ Inharmonic partials at 2.0x, 3.01x, 4.1x, 5.3x.
|
||||
|
||||
pan = score.part("pan", instrument="steel_drum")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_steel_drum.wav" type="audio/wav"></audio>
|
||||
|
||||
Accordion Synth
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -608,6 +756,10 @@ create natural beating. Bellows pressure swell modulates amplitude.
|
||||
|
||||
acc = score.part("acc", instrument="accordion")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_accordion.wav" type="audio/wav"></audio>
|
||||
|
||||
Didgeridoo Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -619,6 +771,10 @@ Best with cave reverb.
|
||||
|
||||
didg = score.part("didg", instrument="didgeridoo")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_didgeridoo.wav" type="audio/wav"></audio>
|
||||
|
||||
Bagpipe Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -629,6 +785,10 @@ peaked around 3-7 (the piercing brightness). No dynamics — always ff.
|
||||
|
||||
pipes = score.part("pipes", instrument="bagpipe")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_bagpipe.wav" type="audio/wav"></audio>
|
||||
|
||||
Banjo Synth
|
||||
~~~~~~~~~~~
|
||||
|
||||
@@ -639,6 +799,10 @@ nasal, ringy resonance with faster decay than guitar.
|
||||
|
||||
banjo = score.part("banjo", instrument="banjo")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_banjo.wav" type="audio/wav"></audio>
|
||||
|
||||
Mandolin Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -649,6 +813,10 @@ doubled unison strings. Bright, ringing, fast attack.
|
||||
|
||||
mando = score.part("mando", instrument="mandolin")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_mandolin.wav" type="audio/wav"></audio>
|
||||
|
||||
Ukulele Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -659,6 +827,10 @@ softer attack than guitar, shorter sustain.
|
||||
|
||||
uke = score.part("uke", instrument="ukulele")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_ukulele.wav" type="audio/wav"></audio>
|
||||
|
||||
Granular Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -688,6 +860,10 @@ Parameters (passed as synth kwargs):
|
||||
- ``source``: Base waveform — ``"saw"``, ``"sine"``, ``"triangle"``,
|
||||
``"square"``, ``"noise"``.
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.3em 0 0.5em"><source src="../_static/audio/synth_granular.wav" type="audio/wav"></audio>
|
||||
|
||||
Analog Oscillator Drift
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
+20
-10
@@ -46,23 +46,33 @@ it through your speakers, export MIDI, finish in your DAW:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import Score, Pattern, Key, Duration, Chord
|
||||
from pytheory import Score, Key, Duration
|
||||
from pytheory.play import play_score
|
||||
|
||||
score = Score("4/4", bpm=140)
|
||||
score.drums("bossa nova", repeats=4)
|
||||
score = Score("4/4", bpm=120)
|
||||
score.drums("rock", repeats=8, fill="rock", fill_every=4)
|
||||
|
||||
chords = score.part("chords", synth="fm", envelope="pad", reverb=0.4)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck", delay=0.3)
|
||||
bass = score.part("bass", synth="sine", lowpass=500)
|
||||
piano = score.part("piano", instrument="piano", reverb=0.3)
|
||||
lead = score.part("lead", synth="saw", envelope="pluck",
|
||||
delay=0.2, reverb=0.2, lowpass=4000)
|
||||
bass = score.part("bass", synth="triangle", lowpass=900)
|
||||
|
||||
for chord in Key("A", "minor").progression("i", "iv", "V", "i"):
|
||||
chords.add(chord, Duration.WHOLE)
|
||||
for chord in Key("G", "major").progression("I", "V", "vi", "IV") * 2:
|
||||
piano.add(chord, Duration.WHOLE)
|
||||
|
||||
lead.arpeggio("Am", bars=4, pattern="updown", octaves=2)
|
||||
lead.add("D5", 1).add("B4", 0.5).add("D5", 0.5)
|
||||
lead.add("G5", 1).add("E5", 1)
|
||||
lead.add("D5", 0.5).add("B4", 0.5).add("A4", 1)
|
||||
lead.add("G4", 2).rest(2)
|
||||
|
||||
for n in ["G2", "G2", "D2", "D2", "E2", "E2", "C2", "C2"] * 2:
|
||||
bass.add(n, Duration.HALF)
|
||||
|
||||
play_score(score)
|
||||
score.save_midi("sketch.mid")
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<audio controls style="width:100%;margin:0.5em 0 1.5em"><source src="_static/audio/quickstart.wav" type="audio/wav"></audio>
|
||||
|
||||
Or hear a randomly generated track from the command line — different
|
||||
every time::
|
||||
|
||||
+206
-10
@@ -543,7 +543,7 @@ def dub_kingston():
|
||||
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,
|
||||
volume=0.15, pan=0.5, ensemble=4,
|
||||
reverb=0.7, reverb_decay=3.0, reverb_type="cathedral",
|
||||
lowpass=1200, detune=10)
|
||||
|
||||
@@ -1124,14 +1124,14 @@ def cinematic_showcase():
|
||||
bells.rest(Duration.WHOLE)
|
||||
|
||||
# String ensemble — lush wide pad
|
||||
strings = score.part("strings", instrument="string_ensemble",
|
||||
strings = score.part("strings", instrument="string_ensemble", ensemble=8,
|
||||
reverb=0.4, reverb_type="hall")
|
||||
strings.rest(Duration.WHOLE)
|
||||
for sym in ["Am", "F", "C", "G", "Dm", "Am", "E"]:
|
||||
strings.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
|
||||
# Cello — deep foundation
|
||||
cello = score.part("cello", instrument="cello",
|
||||
cello = score.part("cello", instrument="cello", ensemble=3,
|
||||
reverb=0.3, reverb_type="hall")
|
||||
cello.rest(Duration.WHOLE)
|
||||
for n in ["A2", "F2", "C3", "G2", "D3", "A2", "E2"]:
|
||||
@@ -1629,7 +1629,7 @@ def epic_bhairav():
|
||||
timp.add(Tone("Pa", octave=2, system=shruti), Duration.HALF, velocity=115)
|
||||
|
||||
# Choir — bar 3
|
||||
choir = score.part("choir", synth="vocal_synth", envelope="pad",
|
||||
choir = score.part("choir", synth="vocal_synth", envelope="pad", ensemble=6,
|
||||
detune=8, spread=0.4, reverb=0.4, reverb_type=REV, volume=0.2)
|
||||
for _ in range(2):
|
||||
choir.rest(Duration.WHOLE)
|
||||
@@ -1651,7 +1651,7 @@ def epic_bhairav():
|
||||
bansuri.add(tone, dur, velocity=vel)
|
||||
|
||||
# Cello — bar 3
|
||||
cello = score.part("cello", instrument="cello", volume=0.22, reverb=0.4, reverb_type=REV)
|
||||
cello = score.part("cello", instrument="cello", volume=0.22, reverb=0.4, reverb_type=REV, ensemble=3)
|
||||
for _ in range(2):
|
||||
cello.rest(Duration.WHOLE)
|
||||
for name, dur, vel in [
|
||||
@@ -1677,7 +1677,7 @@ def epic_bhairav():
|
||||
|
||||
# Strings — bar 13
|
||||
strings = score.part("strings", instrument="string_ensemble", volume=0.18,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
reverb=0.4, reverb_type=REV, ensemble=10)
|
||||
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)]:
|
||||
@@ -1887,7 +1887,7 @@ def ascent():
|
||||
|
||||
# 3: SURFACING (5-8)
|
||||
cello = score.part("cello", instrument="cello", volume=0.22,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
reverb=0.4, reverb_type=REV, ensemble=3)
|
||||
cello.rest(16.0)
|
||||
for note, dur, vel in [("E2",4.0,52),("G2",4.0,55),("B2",4.0,58),("E3",4.0,62)]:
|
||||
cello.add(note, dur, velocity=vel)
|
||||
@@ -1943,7 +1943,7 @@ def ascent():
|
||||
theremin.add(note, dur, velocity=vel)
|
||||
|
||||
strings = score.part("strings", instrument="string_ensemble", volume=0.15,
|
||||
reverb=0.45, reverb_type=REV)
|
||||
reverb=0.45, reverb_type=REV, ensemble=8)
|
||||
strings.rest(40.0)
|
||||
for sym, vel in [("Em",52),("C",55),("Am",58),("B",55),("Em",60),("C",62)]:
|
||||
strings.add(Chord.from_symbol(sym), 4.0, velocity=vel)
|
||||
@@ -2218,7 +2218,7 @@ def pop_rock():
|
||||
cabinet=1.0, cabinet_brightness=0.6,
|
||||
reverb=0.2, reverb_type="plate", pan=0.2)
|
||||
strings = score.part("strings", instrument="string_ensemble", volume=0.12,
|
||||
reverb=0.35, reverb_type="hall")
|
||||
reverb=0.35, reverb_type="hall", ensemble=6)
|
||||
|
||||
prog = ["G", "D", "Em", "C"]
|
||||
|
||||
@@ -2804,6 +2804,201 @@ def snare_cadence():
|
||||
play_song(score, "Snare Cadence — full drumline (8 snares, 4 quads, 5 basses)")
|
||||
|
||||
|
||||
def ensemble_showcase():
|
||||
"""Ensemble Showcase — acid bass, tabla solo, strings, snare line."""
|
||||
score = Score("4/4", bpm=128)
|
||||
|
||||
# ── Drums: house kit ──
|
||||
score.drums("house", repeats=24, fill="house", fill_every=8)
|
||||
score.set_drum_effects(volume=0.4, reverb=0.1)
|
||||
|
||||
# ── 303 Acid Bass: detuned, spread, LFO filter, ensemble=3 ──
|
||||
acid = score.part("acid", synth="saw", volume=0.55, ensemble=3,
|
||||
lowpass=400, lowpass_q=10.0, distortion=0.35,
|
||||
distortion_drive=4.0, legato=True, glide=0.03,
|
||||
detune=12, spread=0.4, sub_osc=0.15,
|
||||
sidechain=0.5, sidechain_release=0.08)
|
||||
|
||||
acid.lfo("lowpass", rate=0.5, min=400, max=5000, bars=16, shape="sine")
|
||||
for _ in range(8):
|
||||
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
|
||||
acid.add(n, Duration.EIGHTH, velocity=90)
|
||||
|
||||
acid.ramp(over=Duration.WHOLE * 8, curve="ease_in", lowpass=6000)
|
||||
for _ in range(8):
|
||||
for n in ["C2", "Eb3", "C3", "G2", "Bb2", "C3", "G2", "C2"]:
|
||||
acid.add(n, Duration.EIGHTH, velocity=95)
|
||||
|
||||
acid.ramp(over=Duration.WHOLE * 8, curve="ease_out", lowpass=500)
|
||||
for _ in range(8):
|
||||
for n in ["C2", "C3", "C2", "Eb2", "C2", "G2", "Bb2", "C2"]:
|
||||
acid.add(n, Duration.EIGHTH, velocity=88)
|
||||
|
||||
# ── Strings: 16-player ensemble pad ──
|
||||
strings = score.part("strings", instrument="string_ensemble", volume=0.0,
|
||||
reverb=0.4, ensemble=16, detune=8, spread=0.5)
|
||||
|
||||
for _ in range(32):
|
||||
strings.rest(Duration.QUARTER)
|
||||
strings.ramp(over=Duration.WHOLE * 4, curve="ease_in", volume=0.18)
|
||||
for ch in ["Cm", "Ab", "Eb", "Bb"] * 4:
|
||||
strings.add(Chord.from_symbol(ch), Duration.WHOLE, velocity=55)
|
||||
strings.ramp(over=Duration.WHOLE * 4, curve="ease_out", volume=0.0)
|
||||
for ch in ["Cm", "Ab", "Eb", "Bb"]:
|
||||
strings.add(Chord.from_symbol(ch), Duration.WHOLE, velocity=45)
|
||||
for _ in range(16):
|
||||
strings.rest(Duration.QUARTER)
|
||||
|
||||
# ── Tabla: ensemble=3, enters bar 9 ──
|
||||
tabla = score.part("tabla", synth="sine", volume=0.0, reverb=0.15, ensemble=3)
|
||||
|
||||
for _ in range(32):
|
||||
tabla.rest(Duration.QUARTER)
|
||||
tabla.ramp(over=Duration.WHOLE * 2, volume=0.45)
|
||||
|
||||
# Keherwa groove — 8 bars
|
||||
for _ in range(8):
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=95, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=50)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=82)
|
||||
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=78)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=88, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_GE, Duration.EIGHTH, velocity=72)
|
||||
|
||||
# Tabla solo — getting busier
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=100, articulation="marcato")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=45)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=85)
|
||||
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=80)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=45)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=55)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=95, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.EIGHTH, velocity=82)
|
||||
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=105, articulation="marcato")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=55)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=45)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=48)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=88)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.SIXTEENTH, velocity=100)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=50)
|
||||
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.EIGHTH, velocity=85)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=108, articulation="accent")
|
||||
|
||||
# Tihai crescendo
|
||||
for vel in [85, 90, 95, 100, 105, 110]:
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=vel, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.SIXTEENTH, velocity=int(vel * 0.55))
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.SIXTEENTH, velocity=int(vel * 0.7))
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.QUARTER, velocity=125, articulation="fermata")
|
||||
tabla.hit(DrumSound.TABLA_GE_BEND, Duration.QUARTER, velocity=110)
|
||||
|
||||
# Groove out
|
||||
for _ in range(4):
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=88, articulation="accent")
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=45)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=42)
|
||||
tabla.hit(DrumSound.TABLA_NA, Duration.EIGHTH, velocity=75)
|
||||
tabla.hit(DrumSound.TABLA_TIN, Duration.EIGHTH, velocity=70)
|
||||
tabla.hit(DrumSound.TABLA_TIT, Duration.EIGHTH, velocity=42)
|
||||
tabla.hit(DrumSound.TABLA_DHA, Duration.EIGHTH, velocity=80)
|
||||
tabla.hit(DrumSound.TABLA_GE, Duration.EIGHTH, velocity=65)
|
||||
|
||||
# ── Snare line: 8-player ensemble, enters bar 17 ──
|
||||
S = DrumSound.MARCH_SNARE
|
||||
R = DrumSound.MARCH_RIMSHOT
|
||||
|
||||
snares = score.part("snares", synth="sine", volume=0.0, reverb=0.15, ensemble=8)
|
||||
|
||||
for _ in range(64):
|
||||
snares.rest(Duration.QUARTER)
|
||||
snares.ramp(over=Duration.WHOLE * 2, volume=0.7)
|
||||
|
||||
for _ in range(4):
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=115)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=118)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=35)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=120)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
|
||||
# Buzz roll finale
|
||||
for i in range(64):
|
||||
snares.hit(S, 0.0625, velocity=min(20 + i * 1.5, 100))
|
||||
snares.hit(R, Duration.EIGHTH, velocity=127)
|
||||
snares.hit(R, Duration.EIGHTH, velocity=127)
|
||||
|
||||
snares.ramp(over=Duration.WHOLE * 2, curve="ease_out", volume=0.0)
|
||||
for _ in range(2):
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=110)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=108)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=105)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
snares.hit(R, Duration.SIXTEENTH, velocity=100)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=30)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=28)
|
||||
snares.hit(S, Duration.SIXTEENTH, velocity=32)
|
||||
|
||||
# ── Lead synth: 6-player ensemble, enters bar 5 ──
|
||||
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.0,
|
||||
lowpass=3500, detune=6, chorus=0.1, reverb=0.2,
|
||||
delay=0.15, delay_time=0.33, delay_feedback=0.25,
|
||||
ensemble=6)
|
||||
|
||||
for _ in range(16):
|
||||
lead.rest(Duration.QUARTER)
|
||||
lead.ramp(over=Duration.WHOLE * 2, volume=0.3)
|
||||
|
||||
for _ in range(2):
|
||||
lead.add("Eb5", Duration.QUARTER, velocity=88)
|
||||
lead.add("G5", Duration.QUARTER, velocity=92)
|
||||
lead.add("Bb5", Duration.HALF, velocity=95, articulation="accent")
|
||||
lead.add("Ab5", Duration.QUARTER, velocity=88)
|
||||
lead.add("G5", Duration.QUARTER, velocity=85)
|
||||
lead.add("Eb5", Duration.QUARTER, velocity=82)
|
||||
lead.add("D5", Duration.QUARTER, velocity=80)
|
||||
|
||||
lead.swell(["Eb5", "G5", "Bb5", "C6", "Bb5", "G5", "Eb5", "D5"],
|
||||
Duration.QUARTER, low_vel=75, peak_vel=105)
|
||||
lead.decrescendo(["Eb5", "D5", "C5", "Bb4"], Duration.HALF,
|
||||
start_vel=90, end_vel=60)
|
||||
|
||||
for _ in range(16):
|
||||
lead.rest(Duration.QUARTER)
|
||||
|
||||
lead.ramp(over=Duration.WHOLE * 2, volume=0.35)
|
||||
lead.crescendo(["C5", "Eb5", "G5", "Bb5", "C6", "Eb6", "C6", "Bb5"],
|
||||
Duration.QUARTER, start_vel=80, end_vel=110)
|
||||
lead.add("G5", Duration.HALF, velocity=105, bend=1, bend_type="smooth")
|
||||
lead.add("Eb5", Duration.HALF, velocity=95)
|
||||
lead.decrescendo(["C5", "Bb4", "G4", "Eb4"], Duration.WHOLE,
|
||||
start_vel=85, end_vel=40)
|
||||
|
||||
play_song(score, "Ensemble Showcase — acid bass, tabla solo, 16-player strings, 8-player snare line")
|
||||
|
||||
|
||||
SONGS = {
|
||||
"1": ("Bossa Nova in A minor", bossa_nova_girl),
|
||||
"2": ("Bebop in Bb major", bebop_in_bb),
|
||||
@@ -2837,6 +3032,7 @@ SONGS = {
|
||||
"30": ("Sitar Drone (Bhairav, hold() polyphony)", sitar_drone),
|
||||
"31": ("Acid Tabla (303 + tabla, ramp, articulations)", acid_tabla),
|
||||
"32": ("Snare Cadence (marching snare, flams, diddles)", snare_cadence),
|
||||
"33": ("Ensemble Showcase (acid+tabla+strings+snare line)", ensemble_showcase),
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -2850,7 +3046,7 @@ if __name__ == "__main__":
|
||||
print(f" {key:>2}. {name}")
|
||||
|
||||
print()
|
||||
choice = input(" Pick a song (1-32, or 'all'): ").strip()
|
||||
choice = input(" Pick a song (1-33, or 'all'): ").strip()
|
||||
print()
|
||||
|
||||
if choice == "all":
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.39.2"
|
||||
version = "0.40.0"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.39.2"
|
||||
__version__ = "0.40.0"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
+297
-140
@@ -358,27 +358,42 @@ def piano_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
# Harmonics with the metallic spectral shape of steel strings
|
||||
n_harmonics = min(15, int((SAMPLE_RATE / 2) / hz))
|
||||
|
||||
# Vectorized harmonic synthesis — all harmonics at once
|
||||
harmonics = numpy.arange(1, n_harmonics + 1, dtype=numpy.float64)
|
||||
|
||||
# Piano spectral shape as array
|
||||
amps = numpy.zeros(n_harmonics, dtype=numpy.float64)
|
||||
amps[0] = 1.0
|
||||
if n_harmonics > 1:
|
||||
amps[1] = 0.7 + 0.15 * brightness
|
||||
if n_harmonics > 2:
|
||||
amps[2] = 0.45 + 0.2 * brightness
|
||||
for i in range(3, min(6, n_harmonics)):
|
||||
amps[i] = (0.25 + 0.15 * brightness) / (i + 1)
|
||||
for i in range(6, n_harmonics):
|
||||
amps[i] = (0.1 + 0.1 * brightness) / ((i + 1) ** 2)
|
||||
|
||||
# Per-harmonic decay rates
|
||||
h_decay_rates = (1.5 - 0.5 * brightness) * (harmonics - 1)
|
||||
|
||||
# Random phases
|
||||
phases = rng.uniform(0, 2 * numpy.pi, n_harmonics)
|
||||
|
||||
for string_hz in [hz, hz2]:
|
||||
for n in range(1, n_harmonics + 1):
|
||||
f_n = string_hz * n
|
||||
if f_n >= SAMPLE_RATE / 2:
|
||||
break
|
||||
# Piano spectral shape: strong 1-3, then falling
|
||||
# Upper register has more prominent harmonics (brighter)
|
||||
if n == 1:
|
||||
amp = 1.0
|
||||
elif n == 2:
|
||||
amp = 0.7 + 0.15 * brightness
|
||||
elif n == 3:
|
||||
amp = 0.45 + 0.2 * brightness
|
||||
elif n <= 6:
|
||||
amp = (0.25 + 0.15 * brightness) / n
|
||||
else:
|
||||
amp = (0.1 + 0.1 * brightness) / (n * n)
|
||||
# Higher harmonics decay faster, but less so in upper register
|
||||
h_decay = decay * numpy.exp(-(1.5 - 0.5 * brightness) * (n - 1) * t)
|
||||
phase = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase) * h_decay
|
||||
freqs = string_hz * harmonics # (n_harmonics,)
|
||||
# Mask out harmonics above Nyquist
|
||||
valid = freqs < SAMPLE_RATE / 2
|
||||
if not valid.any():
|
||||
continue
|
||||
v_freqs = freqs[valid]
|
||||
v_amps = amps[valid]
|
||||
v_rates = h_decay_rates[valid]
|
||||
v_phases = phases[valid]
|
||||
|
||||
# 2D: (n_valid, n_samples) — one sin() call for all harmonics
|
||||
phase_matrix = 2 * numpy.pi * v_freqs[:, numpy.newaxis] * t[numpy.newaxis, :] + v_phases[:, numpy.newaxis]
|
||||
decay_matrix = decay[numpy.newaxis, :] * numpy.exp(-v_rates[:, numpy.newaxis] * t[numpy.newaxis, :])
|
||||
wave += (v_amps[:, numpy.newaxis] * numpy.sin(phase_matrix) * decay_matrix).sum(axis=0)
|
||||
|
||||
wave *= 0.5
|
||||
|
||||
@@ -401,6 +416,68 @@ def piano_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def rhodes_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Rhodes electric piano — tine struck by hammer, electromagnetic pickup.
|
||||
|
||||
The Rhodes sound comes from a rubber-tipped hammer hitting a thin
|
||||
steel tine next to a tonebar. The tine vibrates near an electromagnetic
|
||||
pickup (like a guitar pickup), producing a warm, bell-like tone with:
|
||||
1. Strong fundamental + 2nd harmonic (tine character)
|
||||
2. Bright metallic attack that mellows quickly (hammer on tine)
|
||||
3. Bell-like inharmonic partials on soft hits, bark on hard hits
|
||||
4. Asymmetric waveform from the pickup's nonlinear response
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# Two-stage decay: quick initial drop, then long sustain
|
||||
decay = numpy.where(t < 0.15,
|
||||
numpy.exp(-4.0 * t),
|
||||
numpy.exp(-4.0 * 0.15) * numpy.exp(-1.2 * (t - 0.15)))
|
||||
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
|
||||
# Brightness scales with pitch
|
||||
brightness = numpy.clip((hz - 65) / 800, 0.0, 1.0)
|
||||
|
||||
# Tine harmonics — strong fundamental, prominent 2nd, bell-like upper
|
||||
tine_harmonics = [
|
||||
(1, 1.0), # fundamental
|
||||
(2, 0.6 + 0.15 * brightness), # 2nd — the Rhodes character
|
||||
(3, 0.15 + 0.1 * brightness), # 3rd — adds warmth
|
||||
(4, 0.08 + 0.08 * brightness), # 4th — bell quality
|
||||
(5, 0.04), # 5th — subtle shimmer
|
||||
(6, 0.02), # 6th
|
||||
]
|
||||
|
||||
for n, amp in tine_harmonics:
|
||||
f_n = hz * n
|
||||
if f_n >= SAMPLE_RATE / 2:
|
||||
break
|
||||
# Higher harmonics decay faster
|
||||
h_decay = decay * numpy.exp(-(0.8 + 0.3 * brightness) * (n - 1) * t)
|
||||
phase = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase) * h_decay
|
||||
|
||||
# Hammer-on-tine transient — bright metallic click
|
||||
click_len = min(int(SAMPLE_RATE * 0.008), n_samples)
|
||||
click_t = numpy.arange(click_len, dtype=numpy.float64) / SAMPLE_RATE
|
||||
# Inharmonic tine ring at attack (bell partials)
|
||||
click = (numpy.sin(2 * numpy.pi * hz * 5.3 * click_t) * 0.15 +
|
||||
numpy.sin(2 * numpy.pi * hz * 7.1 * click_t) * 0.08)
|
||||
click *= numpy.exp(-numpy.linspace(0, 20, click_len))
|
||||
wave[:click_len] += click
|
||||
|
||||
# Subtle asymmetry from pickup nonlinearity (soft saturation)
|
||||
wave = numpy.tanh(wave * 1.2) / 1.2
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
wave /= mx
|
||||
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def bass_guitar_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Bass guitar — plucked thick string with magnetic pickup.
|
||||
|
||||
@@ -718,45 +795,62 @@ def cello_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
|
||||
|
||||
def harp_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Harp — plucked string with large resonant soundboard.
|
||||
"""Harp — pure, singing tone with gentle pluck and long sustain.
|
||||
|
||||
Warmer than guitar, longer sustain, with the distinctive
|
||||
"bloom" as the soundboard absorbs and re-radiates energy.
|
||||
Nylon/gut strings on a large resonant frame. The tone is warm
|
||||
and clean — mostly fundamental with gentle upper harmonics that
|
||||
decay faster, leaving a pure singing sustain.
|
||||
"""
|
||||
period = int(SAMPLE_RATE / hz)
|
||||
if period < 2:
|
||||
period = 2
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# Soft pluck — fingers, not pick
|
||||
buf = rng.uniform(-0.5, 0.5, period).astype(numpy.float64)
|
||||
for _ in range(3):
|
||||
# Long, gentle decay
|
||||
decay = numpy.exp(-1.5 * t)
|
||||
|
||||
wave = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
|
||||
# Clean harmonics — strong fundamental, gentle upper partials
|
||||
n_harmonics = min(10, int((SAMPLE_RATE / 2) / hz))
|
||||
for n in range(1, n_harmonics + 1):
|
||||
f_n = hz * n
|
||||
if f_n >= SAMPLE_RATE / 2:
|
||||
break
|
||||
# Fundamental dominates, upper partials gentle and fast-decaying
|
||||
if n == 1:
|
||||
amp = 1.0
|
||||
elif n == 2:
|
||||
amp = 0.3
|
||||
elif n == 3:
|
||||
amp = 0.15
|
||||
else:
|
||||
amp = 0.06 / n
|
||||
h_decay = decay * numpy.exp(-1.5 * (n - 1) * t)
|
||||
phase = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase) * h_decay
|
||||
|
||||
# Karplus-Strong pluck transient — just the first ~50ms
|
||||
# Gives the finger-on-string attack, then the pure tone takes over
|
||||
period = max(2, int(SAMPLE_RATE / hz))
|
||||
ks_rng = numpy.random.default_rng(int(hz * 77) % 2**31)
|
||||
ks_buf = ks_rng.uniform(-0.3, 0.3, period).astype(numpy.float64)
|
||||
# Pre-filter for soft finger pluck
|
||||
for _ in range(4):
|
||||
for k in range(period - 1):
|
||||
buf[k] = 0.6 * buf[k] + 0.4 * buf[k + 1]
|
||||
ks_buf[k] = 0.6 * ks_buf[k] + 0.4 * ks_buf[k + 1]
|
||||
pluck_len = min(int(SAMPLE_RATE * 0.05), n_samples)
|
||||
pluck = numpy.zeros(pluck_len, dtype=numpy.float64)
|
||||
for i in range(pluck_len):
|
||||
pluck[i] = ks_buf[i % period]
|
||||
nxt = (i + 1) % period
|
||||
ks_buf[i % period] = 0.5 * (ks_buf[i % period] + ks_buf[nxt])
|
||||
# Fade the KS pluck out quickly so the pure tone takes over
|
||||
pluck *= numpy.exp(-numpy.linspace(0, 8, pluck_len)) * 0.4
|
||||
wave[:pluck_len] += pluck
|
||||
|
||||
out = numpy.zeros(n_samples, dtype=numpy.float64)
|
||||
for i in range(n_samples):
|
||||
out[i] = buf[i % period]
|
||||
next_idx = (i + 1) % period
|
||||
# Long sustain — harp strings ring
|
||||
buf[i % period] = 0.5 * (buf[i % period] + buf[next_idx]) * 0.9995
|
||||
|
||||
# Large soundboard resonance — bloom effect
|
||||
for center, bw, gain in [(150, 60, 0.3), (350, 100, 0.25), (700, 150, 0.15)]:
|
||||
lo = max(20, center - bw)
|
||||
hi = min(SAMPLE_RATE // 2 - 1, center + bw)
|
||||
if lo < hi:
|
||||
bp, ap = scipy.signal.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
out += scipy.signal.lfilter(bp, ap, out) * gain
|
||||
|
||||
# Warm rolloff
|
||||
bl, al = scipy.signal.butter(2, min(5000, hz * 10), btype='low', fs=SAMPLE_RATE)
|
||||
out = scipy.signal.lfilter(bl, al, out)
|
||||
|
||||
mx = numpy.abs(out).max()
|
||||
mx = numpy.abs(wave).max()
|
||||
if mx > 0:
|
||||
out /= mx
|
||||
return (peak * out).astype(numpy.int16)
|
||||
wave /= mx
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def upright_bass_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
@@ -817,11 +911,19 @@ def timpani_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
wave += numpy.sin(2 * numpy.pi * hz * 1.99 * t) * 0.2 * numpy.exp(-10 * t)
|
||||
wave += numpy.sin(2 * numpy.pi * hz * 2.44 * t) * 0.1 * numpy.exp(-15 * t)
|
||||
|
||||
# Two-stage decay: initial thump fades fast, fundamental rings
|
||||
decay = numpy.where(t < 0.15,
|
||||
numpy.exp(-4 * t),
|
||||
numpy.exp(-4 * 0.15) * numpy.exp(-1.5 * (t - 0.15)))
|
||||
wave *= decay
|
||||
# Two-stage decay: thump fades, but fundamental SUSTAINS long
|
||||
# This is what makes the "oooh" — the fundamental rings and rings,
|
||||
# so rapid hits in a roll stack into a singing resonance
|
||||
thump_decay = numpy.exp(-6 * t) # upper modes die fast
|
||||
fund_decay = numpy.exp(-0.35 * t) # fundamental sustains very long
|
||||
|
||||
# Apply different decays: fundamental gets long sustain
|
||||
fund = numpy.sin(2 * numpy.pi * hz * t) * 0.8 * fund_decay
|
||||
upper = (numpy.sin(2 * numpy.pi * hz * 1.5 * t) * 0.35 * numpy.exp(-6 * t) +
|
||||
numpy.sin(2 * numpy.pi * hz * 1.99 * t) * 0.2 * numpy.exp(-10 * t) +
|
||||
numpy.sin(2 * numpy.pi * hz * 2.44 * t) * 0.1 * numpy.exp(-15 * t))
|
||||
upper *= thump_decay
|
||||
wave = fund + upper
|
||||
|
||||
# Felt mallet impact — warm, not sharp
|
||||
mallet_len = min(int(SAMPLE_RATE * 0.02), n_samples)
|
||||
@@ -830,12 +932,11 @@ def timpani_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
mallet *= numpy.exp(-numpy.linspace(0, 8, mallet_len))
|
||||
wave[:mallet_len] += mallet
|
||||
|
||||
# Copper kettle resonance — boosts low-mids
|
||||
import scipy.signal as _sig
|
||||
lo, hi = max(20, int(hz * 0.7)), min(SAMPLE_RATE // 2 - 1, int(hz * 2))
|
||||
# Copper kettle resonance — boosts the fundamental ring
|
||||
lo, hi = max(20, int(hz * 0.7)), min(SAMPLE_RATE // 2 - 1, int(hz * 1.3))
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
kettle = _sig.lfilter(bp, ap, wave) * 0.3
|
||||
bp, ap = scipy.signal.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
kettle = scipy.signal.lfilter(bp, ap, wave) * 0.4
|
||||
wave += kettle
|
||||
|
||||
mx = numpy.abs(wave).max()
|
||||
@@ -1857,7 +1958,7 @@ _SYNTH_FUNCTIONS = {
|
||||
"noise": noise_wave, "supersaw": supersaw_wave,
|
||||
"pwm_slow": pwm_slow_wave, "pwm_fast": pwm_fast_wave,
|
||||
"pluck_synth": pluck_wave, "organ_synth": organ_wave,
|
||||
"strings_synth": strings_wave, "piano_synth": piano_wave,
|
||||
"strings_synth": strings_wave, "piano_synth": piano_wave, "rhodes_synth": rhodes_wave,
|
||||
"bass_guitar_synth": bass_guitar_wave, "flute_synth": flute_wave,
|
||||
"trumpet_synth": trumpet_wave, "clarinet_synth": clarinet_wave,
|
||||
"marimba_synth": marimba_wave, "oboe_synth": oboe_wave,
|
||||
@@ -1995,16 +2096,32 @@ def _noise(n_samples):
|
||||
return numpy.random.uniform(-1.0, 1.0, n_samples).astype(numpy.float32)
|
||||
|
||||
|
||||
# ── Cached helpers for hot paths ──────────────────────────────────────────
|
||||
|
||||
_time_cache = {}
|
||||
|
||||
|
||||
def _get_time_array(n_samples):
|
||||
"""Cached time array — avoids reallocation on every synth call."""
|
||||
if n_samples not in _time_cache:
|
||||
_time_cache[n_samples] = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
return _time_cache[n_samples]
|
||||
|
||||
|
||||
def _sine_f32(hz, n_samples):
|
||||
"""Float32 sine wave, normalized to ±1."""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
return numpy.sin(2 * numpy.pi * hz * t)
|
||||
return numpy.sin(2 * numpy.pi * hz * _get_time_array(n_samples))
|
||||
|
||||
|
||||
_decay_cache = {}
|
||||
|
||||
|
||||
def _exp_decay(n_samples, decay_rate):
|
||||
"""Exponential decay envelope from 1→0."""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
return numpy.exp(-decay_rate * t)
|
||||
"""Exponential decay envelope from 1→0. Cached."""
|
||||
key = (n_samples, decay_rate)
|
||||
if key not in _decay_cache:
|
||||
_decay_cache[key] = numpy.exp(-decay_rate * _get_time_array(n_samples))
|
||||
return _decay_cache[key]
|
||||
|
||||
|
||||
def _synth_kick(n_samples):
|
||||
@@ -2426,34 +2543,36 @@ def _synth_tabla_ke(n_samples):
|
||||
|
||||
|
||||
def _synth_dhol_dagga(n_samples):
|
||||
"""Dhol dagga — heavy bass side hit with thick stick.
|
||||
"""Dhol dagga — thunderous bass side hit with thick stick.
|
||||
|
||||
The dhol's bass head is thick goatskin, hit with a heavy curved
|
||||
stick (dagga). Massive low-end punch, the sound of bhangra.
|
||||
stick (dagga). Massive, thunderous low-end — the kind of hit
|
||||
you feel in your chest before you hear it.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Heavy membrane thud
|
||||
thump_len = min(int(SAMPLE_RATE * 0.08), n_samples)
|
||||
# Heavy membrane thud — longer, wider band
|
||||
thump_len = min(int(SAMPLE_RATE * 0.12), n_samples)
|
||||
thump_raw = _noise(thump_len)
|
||||
if thump_len > 20:
|
||||
bl, al = scipy.signal.butter(2, [30, 180], btype='band', fs=SAMPLE_RATE)
|
||||
bl, al = scipy.signal.butter(2, [25, 150], btype='band', fs=SAMPLE_RATE)
|
||||
thump = scipy.signal.lfilter(bl, al, numpy.pad(thump_raw, (0, max(0, n_samples - thump_len))))[:thump_len]
|
||||
else:
|
||||
thump = thump_raw
|
||||
thump *= _exp_decay(thump_len, 15) * 1.0
|
||||
# Deep pitched body — lower than tabla bayan
|
||||
freq = 50 + 60 * numpy.exp(-20 * t)
|
||||
thump *= _exp_decay(thump_len, 10) * 1.2
|
||||
# Deep pitched body with pitch sweep — thunderous boom
|
||||
freq = 45 + 80 * numpy.exp(-15 * t)
|
||||
phase = 2 * numpy.pi * numpy.cumsum(freq) / SAMPLE_RATE
|
||||
body = numpy.sin(phase) * _exp_decay(n_samples, 8) * 0.9
|
||||
# Sub boom
|
||||
sub = _sine_f32(35, n_samples) * _exp_decay(n_samples, 10) * 0.6
|
||||
# Stick attack — heavier than tabla palm
|
||||
click_len = min(200, n_samples)
|
||||
click = _noise(click_len) * _exp_decay(click_len, 60) * 0.5
|
||||
result = body + sub
|
||||
body = numpy.sin(phase) * _exp_decay(n_samples, 5) * 1.0
|
||||
# Massive sub boom — sustained
|
||||
sub = _sine_f32(30, n_samples) * _exp_decay(n_samples, 4) * 0.8
|
||||
sub2 = _sine_f32(45, n_samples) * _exp_decay(n_samples, 6) * 0.5
|
||||
# Heavy stick impact
|
||||
click_len = min(300, n_samples)
|
||||
click = _noise(click_len) * _exp_decay(click_len, 40) * 0.6
|
||||
result = body + sub + sub2
|
||||
result[:thump_len] += thump
|
||||
result[:click_len] += click
|
||||
return numpy.tanh(result * 1.5)
|
||||
return numpy.tanh(result * 1.8)
|
||||
|
||||
|
||||
def _synth_dhol_tilli(n_samples):
|
||||
@@ -2485,10 +2604,10 @@ def _synth_dhol_tilli(n_samples):
|
||||
|
||||
|
||||
def _synth_dhol_both(n_samples):
|
||||
"""Dhol both sides — full power bhangra hit."""
|
||||
dagga = _synth_dhol_dagga(n_samples) * 0.6
|
||||
tilli = _synth_dhol_tilli(n_samples) * 0.5
|
||||
return numpy.tanh(dagga + tilli)
|
||||
"""Dhol both sides — full power bhangra hit. Thunderous."""
|
||||
dagga = _synth_dhol_dagga(n_samples) * 0.7
|
||||
tilli = _synth_dhol_tilli(n_samples) * 0.45
|
||||
return numpy.tanh((dagga + tilli) * 1.2)
|
||||
|
||||
|
||||
def _synth_dholak_ge(n_samples):
|
||||
@@ -2673,62 +2792,91 @@ def _synth_doumbek_ka(n_samples):
|
||||
|
||||
|
||||
def _synth_cajon_bass(n_samples):
|
||||
"""Cajón bass — palm strike on center of the face.
|
||||
"""Cajón bass — open palm slaps the center of the plywood face.
|
||||
|
||||
Deep woody thump. The box resonates like a bass drum but with
|
||||
a warmer, more wooden character.
|
||||
Two sounds happening at once: the fleshy THUD of the palm
|
||||
hitting wood, then the box resonating. The hand sound is
|
||||
a soft, round impact — skin on plywood — followed by the
|
||||
hollow chamber boom.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Wooden box thump
|
||||
thump_len = min(int(SAMPLE_RATE * 0.06), n_samples)
|
||||
# HAND IMPACT — fleshy palm on wood, round and thuddy
|
||||
hand_len = min(int(SAMPLE_RATE * 0.015), n_samples)
|
||||
hand_raw = _noise(hand_len)
|
||||
if hand_len > 10:
|
||||
# Lowpassed — palm is soft, not bright
|
||||
bl, al = scipy.signal.butter(2, 1500 / (SAMPLE_RATE / 2), btype='low')
|
||||
hand = scipy.signal.lfilter(bl, al, numpy.pad(hand_raw, (0, max(0, n_samples - hand_len))))[:hand_len].astype(numpy.float32)
|
||||
else:
|
||||
hand = hand_raw
|
||||
hand *= _exp_decay(hand_len, 100) * 1.0
|
||||
# BOX RESONANCE — hollow chamber thud
|
||||
body = numpy.sin(2 * numpy.pi * 75 * t) * _exp_decay(n_samples, 5) * 0.7
|
||||
box1 = numpy.sin(2 * numpy.pi * 170 * t) * _exp_decay(n_samples, 10) * 0.4
|
||||
box2 = numpy.sin(2 * numpy.pi * 300 * t) * _exp_decay(n_samples, 16) * 0.2
|
||||
# Panel flex — deep sub thud
|
||||
sub = _sine_f32(45, n_samples) * _exp_decay(n_samples, 6) * 0.5
|
||||
# Broader thump from the air cavity
|
||||
thump_len = min(int(SAMPLE_RATE * 0.1), 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)
|
||||
bl, al = scipy.signal.butter(2, [40, 350], btype='band', fs=SAMPLE_RATE)
|
||||
thump = scipy.signal.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
|
||||
thump *= _exp_decay(thump_len, 12) * 0.6
|
||||
result = body + box1 + box2 + 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
|
||||
result[:hand_len] += hand
|
||||
return numpy.tanh(result * 1.5).astype(numpy.float32)
|
||||
|
||||
|
||||
def _synth_cajon_slap(n_samples):
|
||||
"""Cajón slap — fingers near the top edge, pure wood crack.
|
||||
|
||||
No snare wires. Just the sharp crack of fingers on the plywood
|
||||
edge with the box resonance underneath. Dry and woody.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
# Wood panel resonance — boxy mid
|
||||
body = numpy.sin(2 * numpy.pi * 240 * t) * _exp_decay(n_samples, 28) * 0.4
|
||||
box = numpy.sin(2 * numpy.pi * 400 * t) * _exp_decay(n_samples, 38) * 0.2
|
||||
box2 = numpy.sin(2 * numpy.pi * 600 * t) * _exp_decay(n_samples, 50) * 0.1
|
||||
# Sharp edge slap — fingers on plywood
|
||||
slap_len = min(int(SAMPLE_RATE * 0.004), n_samples)
|
||||
slap = _noise(slap_len) * _exp_decay(slap_len, 300) * 1.0
|
||||
result = body + box + box2
|
||||
result[:slap_len] += slap
|
||||
return numpy.tanh(result * 1.8).astype(numpy.float32)
|
||||
|
||||
|
||||
def _synth_cajon_slap_snare(n_samples):
|
||||
"""Cajón slap with snare wires — the buzzy version.
|
||||
|
||||
Same edge slap but with internal snare wires rattling.
|
||||
"""
|
||||
wood = _synth_cajon_slap(n_samples)
|
||||
# Add snare wire buzz on top
|
||||
wire_len = min(int(SAMPLE_RATE * 0.06), n_samples)
|
||||
wire = _noise(wire_len) * _exp_decay(wire_len, 20) * 0.45
|
||||
if wire_len > 20:
|
||||
bl, al = scipy.signal.butter(2, [1500, 5000], btype='band', fs=SAMPLE_RATE)
|
||||
wire = scipy.signal.lfilter(bl, al, numpy.pad(wire, (0, max(0, n_samples - wire_len))))[:wire_len].astype(numpy.float32)
|
||||
result = wood.copy()
|
||||
result[:wire_len] += wire
|
||||
return numpy.tanh(result * 1.2).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))
|
||||
"""Cajón tap — light fingertip on the plywood face. Ghost note."""
|
||||
n = min(n_samples, int(SAMPLE_RATE * 0.05))
|
||||
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
|
||||
# Finger on wood — hollow tap
|
||||
tap = numpy.sin(2 * numpy.pi * 280 * t) * _exp_decay(n, 40) * 0.3
|
||||
box = numpy.sin(2 * numpy.pi * 450 * t) * _exp_decay(n, 55) * 0.12
|
||||
pop = _noise(min(60, n)) * _exp_decay(min(60, n), 200) * 0.4
|
||||
result = tap + box
|
||||
result[:min(60, n)] += pop
|
||||
out = numpy.zeros(n_samples, dtype=numpy.float32)
|
||||
out[:n] = numpy.tanh(result * 1.5)
|
||||
return out
|
||||
@@ -3152,6 +3300,7 @@ def _render_drum_hit(sound_value, n_samples):
|
||||
# Cajon
|
||||
DrumSound.CAJON_BASS.value: lambda n: _synth_cajon_bass(n),
|
||||
DrumSound.CAJON_SLAP.value: lambda n: _synth_cajon_slap(n),
|
||||
DrumSound.CAJON_SLAP_SNARE.value: lambda n: _synth_cajon_slap_snare(n),
|
||||
DrumSound.CAJON_TAP.value: lambda n: _synth_cajon_tap(n),
|
||||
# Metal kit
|
||||
DrumSound.METAL_KICK.value: lambda n: _synth_metal_kick(n),
|
||||
@@ -3176,7 +3325,20 @@ def _render_drum_hit(sound_value, n_samples):
|
||||
}
|
||||
|
||||
renderer = _dispatch.get(sound_value, lambda n: _synth_clave(n))
|
||||
return renderer(n_samples)
|
||||
result = renderer(n_samples)
|
||||
return result
|
||||
|
||||
|
||||
# Drum hit cache — same sound at same length sounds identical
|
||||
_drum_cache = {}
|
||||
|
||||
|
||||
def _render_drum_hit_cached(sound_value, n_samples):
|
||||
"""Cached version of _render_drum_hit for pattern playback."""
|
||||
key = (sound_value, n_samples)
|
||||
if key not in _drum_cache:
|
||||
_drum_cache[key] = _render_drum_hit(sound_value, n_samples)
|
||||
return _drum_cache[key].copy() # copy so callers can mutate
|
||||
|
||||
|
||||
def _render_pattern(pattern, bpm=120):
|
||||
@@ -3200,7 +3362,7 @@ def _render_pattern(pattern, bpm=120):
|
||||
remaining = total_samples - start
|
||||
# Render each hit for up to 0.5 seconds
|
||||
hit_len = min(int(SAMPLE_RATE * 0.5), remaining)
|
||||
wave = _render_drum_hit(hit.sound.value, hit_len)
|
||||
wave = _render_drum_hit_cached(hit.sound.value, hit_len)
|
||||
vel_scale = hit.velocity / 127.0
|
||||
buf[start:start + hit_len] += wave * vel_scale
|
||||
|
||||
@@ -4477,12 +4639,6 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
vel_cutoff = vel_to_filter * vel_scale + 1000
|
||||
mixed = _apply_lowpass(mixed, vel_cutoff, q=filter_q)
|
||||
end = min(start + len(mixed), total_samples)
|
||||
# Choke: fade out any existing signal at this point
|
||||
# so new notes don't pile up on previous tails
|
||||
choke_len = min(int(SAMPLE_RATE * 0.003), start)
|
||||
if choke_len > 0:
|
||||
fade = numpy.linspace(1.0, 0.0, choke_len).astype(numpy.float32)
|
||||
buf[start - choke_len:start] *= fade
|
||||
buf[start:end] += mixed[:end - start] * volume * vel_scale
|
||||
# Spread detuned oscillators into stereo L/R
|
||||
if detune_up is not None and stereo_buf is not None:
|
||||
@@ -4849,6 +5005,7 @@ def render_score(score):
|
||||
# Cajon — centered (single instrument)
|
||||
DrumSound.CAJON_BASS.value: 0.0,
|
||||
DrumSound.CAJON_SLAP.value: 0.0,
|
||||
DrumSound.CAJON_SLAP_SNARE.value: 0.0,
|
||||
DrumSound.CAJON_TAP.value: 0.1,
|
||||
# Metal kit
|
||||
DrumSound.METAL_KICK.value: 0.0,
|
||||
@@ -4949,7 +5106,7 @@ def render_score(score):
|
||||
|
||||
remaining = total_samples - start
|
||||
hit_len = min(int(SAMPLE_RATE * 0.5), remaining)
|
||||
wave = _render_drum_hit(hit.sound.value, hit_len)
|
||||
wave = _render_drum_hit_cached(hit.sound.value, hit_len)
|
||||
vel = hit.velocity
|
||||
if drum_humanize > 0:
|
||||
vel_jitter = int(drum_humanize * 10)
|
||||
|
||||
+7
-8
@@ -17,13 +17,11 @@ INSTRUMENTS = {
|
||||
"synth": "piano_synth", "envelope": "none",
|
||||
"vel_to_filter": 3000,
|
||||
},
|
||||
"electric_piano": { # Rhodes/Wurlitzer
|
||||
"synth": "fm", "envelope": "piano",
|
||||
"fm_ratio": 1.0, "fm_index": 2.0,
|
||||
"detune": 6, "chorus": 0.2, "chorus_rate": 1.0,
|
||||
"lowpass": 4000, "saturation": 0.15,
|
||||
"tremolo_depth": 0.15, "tremolo_rate": 4.5,
|
||||
"analog": 0.2,
|
||||
"electric_piano": { # Rhodes
|
||||
"synth": "rhodes_synth", "envelope": "none",
|
||||
"chorus": 0.15, "chorus_rate": 1.0,
|
||||
"tremolo_depth": 0.12, "tremolo_rate": 4.5,
|
||||
"analog": 0.15,
|
||||
},
|
||||
"organ": {
|
||||
"synth": "organ_synth", "envelope": "organ",
|
||||
@@ -558,8 +556,9 @@ class DrumSound(Enum):
|
||||
DOUMBEK_KA = 114 # muted edge slap
|
||||
# Cajon sounds
|
||||
CAJON_BASS = 108 # center of face, deep thump
|
||||
CAJON_SLAP = 109 # top edge, snare wires buzz
|
||||
CAJON_SLAP = 109 # top edge, wood crack (no snare wires)
|
||||
CAJON_TAP = 110 # light finger tap
|
||||
CAJON_SLAP_SNARE = 111 # top edge with snare wires engaged
|
||||
# Metal kit — tighter, punchier, more attack
|
||||
METAL_KICK = 105 # clicky, punchy, tight
|
||||
METAL_SNARE = 106 # crack, bright, cutting
|
||||
|
||||
@@ -690,7 +690,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytheory"
|
||||
version = "0.39.2"
|
||||
version = "0.40.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
|
||||
Reference in New Issue
Block a user