mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 938c1cc132 | |||
| 9dc22db4b2 | |||
| f570e226cd | |||
| 0c5c3abedc | |||
| 35d07b984b | |||
| aec7723ee6 | |||
| b98a40297b | |||
| 9117568b74 | |||
| 11e4417c62 | |||
| 4edf1d983d | |||
| 74b07b1a8a | |||
| c9437209a7 | |||
| 92cb855a49 | |||
| f06c6f77d1 | |||
| 51bd63658f |
@@ -2,6 +2,31 @@
|
||||
|
||||
All notable changes to PyTheory are documented here.
|
||||
|
||||
## 0.36.6
|
||||
|
||||
- **6 new drum fills** — 3 cajón (flam, rumble, breakdown) and 3 metal
|
||||
(triplet, blast, cascade). 27 fills total.
|
||||
- Updated drums documentation with fill lists and examples
|
||||
|
||||
## 0.36.5
|
||||
|
||||
- **Duration arithmetic** — `Duration.WHOLE * 2`, `Duration.HALF + Duration.QUARTER`,
|
||||
division, and reverse multiply all work now (previously raised TypeError)
|
||||
|
||||
## 0.36.3
|
||||
|
||||
- **`Part.hold()`** — polyphonic overlap on a single part. Add notes
|
||||
without advancing the beat position so they play simultaneously.
|
||||
Enables: piano sustain, sitar drone under melody, guitar strum texture.
|
||||
- **Strum uses hold()** — leading string plays simultaneously with chord,
|
||||
no more timing gaps or choppiness
|
||||
- **Improved songs** 1-16: humanize, velocity dynamics, reverb, saxophone
|
||||
for blues
|
||||
- **Ctrl-C handling** — clean stop on all playback functions
|
||||
- **REPL updates** — strum, roll, bend, temperament, reference commands
|
||||
- Song #28 Descent (generative), #29 Pop Rock, #30 Sitar Drone
|
||||
- 862 tests
|
||||
|
||||
## 0.36.1
|
||||
|
||||
- **7 new instrument synths:** pedal steel guitar, theremin, kalimba/thumb
|
||||
|
||||
@@ -322,6 +322,14 @@ against 17 known chord types (triads, 7ths, 9ths, sus, power chords).
|
||||
>>> Chord.from_tones("Bb", "D", "F").identify()
|
||||
'Bb major'
|
||||
|
||||
Enharmonic spellings are fully supported — Cb, Fb, E#, B#, double
|
||||
sharps/flats, and unicode symbols (see :doc:`tones` for details):
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Chord.from_tones("Cb", "Eb", "Gb").identify()
|
||||
'B minor'
|
||||
|
||||
You can also access the root and quality separately:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
+68
-11
@@ -9,8 +9,8 @@ in Atlanta. Over a dancehall pattern, you're in Kingston. The drums ARE
|
||||
the genre -- they tell the listener's body how to move before a single
|
||||
melodic note is played.
|
||||
|
||||
PyTheory includes a complete drum system -- 27 synthesized percussion
|
||||
sounds, 80+ pattern presets across dozens of genres, and 21 fill presets.
|
||||
PyTheory includes a complete drum system -- 51 synthesized percussion
|
||||
sounds, 80+ pattern presets across dozens of genres, and 27 fill presets.
|
||||
Every sound is generated from waveforms; no samples needed.
|
||||
|
||||
Drum Sounds
|
||||
@@ -91,7 +91,7 @@ The ``DrumSound`` enum maps to General MIDI percussion note numbers:
|
||||
>>> DrumSound.CLOSED_HAT.value
|
||||
42
|
||||
|
||||
All 27 sounds, organized by type:
|
||||
All 51 sounds, organized by type:
|
||||
|
||||
**Kicks:** KICK (36)
|
||||
|
||||
@@ -106,7 +106,24 @@ All 27 sounds, organized by type:
|
||||
**Percussion:** COWBELL (56), CLAVE (75), SHAKER (70), TAMBOURINE (54),
|
||||
CONGA_HIGH (63), CONGA_LOW (64), BONGO_HIGH (60), BONGO_LOW (61),
|
||||
TIMBALE_HIGH (65), TIMBALE_LOW (66), AGOGO_HIGH (67), AGOGO_LOW (68),
|
||||
GUIRO (73), MARACAS (70)
|
||||
GUIRO (73)
|
||||
|
||||
**Tabla:** TABLA_NA (86), TABLA_TIN (87), TABLA_GE (88), TABLA_DHA (89),
|
||||
TABLA_TIT (90), TABLA_KE (91), TABLA_GE_BEND (108 -- bayan with upward
|
||||
pitch bend from palm pressing into the head)
|
||||
|
||||
**Dhol:** DHOL_DAGGA (92), DHOL_TILLI (93), DHOL_BOTH (94)
|
||||
|
||||
**Dholak:** DHOLAK_GE (95), DHOLAK_NA (96), DHOLAK_TIT (97)
|
||||
|
||||
**Mridangam:** MRIDANGAM_THAM (98), MRIDANGAM_NAM (99), MRIDANGAM_DIN (100),
|
||||
MRIDANGAM_THA (101)
|
||||
|
||||
**Djembe:** DJEMBE_BASS (102), DJEMBE_TONE (103), DJEMBE_SLAP (104)
|
||||
|
||||
**Cajón:** CAJON_SLAP (109), CAJON_TAP (110)
|
||||
|
||||
**Metal Kit:** METAL_KICK (105), METAL_SNARE (106), METAL_HAT (107)
|
||||
|
||||
Drum Synthesis
|
||||
--------------
|
||||
@@ -200,8 +217,8 @@ everything to its essentials. The metal kit adds 3 dedicated sounds
|
||||
(double kick, china cymbal, stack) and 4 patterns for extreme metal
|
||||
subgenres.
|
||||
|
||||
**World Percussion:** tabla, dhol, dholak, mridangam, djembe -- Deep
|
||||
traditions from across the globe, each with authentic sound sets and
|
||||
**World Percussion:** tabla, dhol, dholak, mridangam, djembe, cajón --
|
||||
Deep traditions from across the globe, each with authentic sound sets and
|
||||
idiomatic patterns. See the World Percussion section below for details.
|
||||
|
||||
**Other:** funk, hip hop, bo diddley, second line, new orleans, waltz,
|
||||
@@ -235,14 +252,16 @@ ending and a new one is about to begin. Without fills, a drum pattern
|
||||
just loops. With them, it breathes and has structure.
|
||||
|
||||
``Pattern.fill()`` loads a 1-bar drum fill -- a short break that
|
||||
transitions between sections. 21 fill presets are available:
|
||||
transitions between sections. 27 fill presets are available:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Pattern.list_fills()
|
||||
['afrobeat', 'blast', 'bossa nova', 'breakdown', 'buildup',
|
||||
'cajon breakdown', 'cajon flam', 'cajon rumble',
|
||||
'cumbia', 'disco', 'funk', 'highlife', 'hip hop', 'house',
|
||||
'jazz', 'jazz brush', 'metal', 'reggae', 'rock', 'rock crash',
|
||||
'jazz', 'jazz brush', 'metal', 'metal blast', 'metal cascade',
|
||||
'metal triplet', 'reggae', 'rock', 'rock crash',
|
||||
'salsa', 'samba', 'second line', 'trap']
|
||||
|
||||
>>> fill = Pattern.fill("rock")
|
||||
@@ -330,14 +349,25 @@ most expressive percussion instruments ever created. A single tabla
|
||||
player can produce an astonishing range of tones by varying finger
|
||||
placement, pressure, and striking technique.
|
||||
|
||||
**6 sounds** -- covering the primary tabla strokes (na, tin, tun, ge,
|
||||
ke, and ti-ra-ki-ta combinations).
|
||||
**7 sounds** -- covering the primary tabla strokes (na, tin, tun, ge,
|
||||
dha, ke, tit) plus a bayan pitch bend sound (TABLA_GE_BEND) that
|
||||
models the technique of pressing the palm into the bayan head to bend
|
||||
the pitch upward.
|
||||
|
||||
**7 patterns:** teental (16 beats, the most common taal), jhaptaal
|
||||
(10 beats), rupak (7 beats), dadra (6 beats), keherwa (8 beats, folk
|
||||
and light classical), tabla solo, and tiri kita (fast ornamental
|
||||
pattern).
|
||||
|
||||
**5 fills:** tihai (3x crescendo landing on sam), chakkardar (32nd
|
||||
triplet cascade into slam), tiri kita (rapid 16th-note dayan burst),
|
||||
bayan (deep bass bends showcase), tabla call (dayan/bayan call-and-response).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score.drums("teental", repeats=4, fill="tihai")
|
||||
score.drums("keherwa", repeats=4, fill="chakkardar")
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=80)
|
||||
@@ -428,10 +458,37 @@ metal blast (blast beat with china cymbal accents), metal groove (a
|
||||
half-time groove with double kick fills), and metal gallop (the
|
||||
classic triplet-feel gallop rhythm).
|
||||
|
||||
**4 fills:** metal (double kick 16ths with descending toms), metal triplet
|
||||
(double kick triplets with snare accents), metal blast (alternating
|
||||
snare/kick 32nds into half-time crash), metal cascade (descending snare
|
||||
roll → kick roll → alternating → crash ending).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=200)
|
||||
score.drums("metal blast", repeats=4)
|
||||
score.drums("metal blast", repeats=8, fill="metal cascade", fill_every=4)
|
||||
|
||||
Cajón
|
||||
~~~~~
|
||||
|
||||
The cajón is a box-shaped percussion instrument from Peru, now
|
||||
ubiquitous in acoustic and unplugged settings worldwide. Players sit
|
||||
on the box and strike the front face with their hands.
|
||||
|
||||
**3 sounds** -- bass (deep center thump), slap (sharp, snare-like edge
|
||||
hit with wire buzz), and tap (light finger tap).
|
||||
|
||||
**3 patterns:** cajon (basic groove), cajon rumba (flamenco-style rumba),
|
||||
and cajon folk (folk/acoustic pattern).
|
||||
|
||||
**3 fills:** cajon flam (slaps accelerating into bass hits), cajon rumble
|
||||
(fast taps building to slap accents), cajon breakdown (syncopated
|
||||
bass-slap groove).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100)
|
||||
score.drums("cajon", repeats=8, fill="cajon flam", fill_every=4)
|
||||
|
||||
MIDI Export
|
||||
-----------
|
||||
|
||||
@@ -841,9 +841,11 @@ processes each section independently:
|
||||
lead.arpeggio("Gm", bars=4, pattern="updown", octaves=2)
|
||||
|
||||
Any parameter can be automated: ``lowpass``, ``lowpass_q``, ``highpass``,
|
||||
``reverb``, ``reverb_decay``, ``delay``, ``delay_time``, ``delay_feedback``,
|
||||
``distortion``, ``distortion_drive``, ``chorus``, ``phaser``, ``phaser_rate``,
|
||||
``saturation``, ``tremolo_depth``, ``tremolo_rate``, ``volume``.
|
||||
``reverb``, ``reverb_decay``, ``reverb_type``, ``delay``, ``delay_time``,
|
||||
``delay_feedback``, ``distortion``, ``distortion_drive``, ``chorus``,
|
||||
``phaser``, ``phaser_rate``, ``saturation``, ``tremolo_depth``,
|
||||
``tremolo_rate``, ``cabinet``, ``cabinet_brightness``, ``analog_drift``,
|
||||
``volume``.
|
||||
|
||||
LFO Automation
|
||||
--------------
|
||||
|
||||
+12
-1
@@ -66,6 +66,17 @@ the mix louder and punchier:
|
||||
chords.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||||
play_score(score)
|
||||
|
||||
The render pipeline respects the Score's ``temperament`` and
|
||||
``reference_pitch`` settings, so Baroque or microtonal scores play back
|
||||
at the correct tuning:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=80, temperament="meantone", reference_pitch=415.0)
|
||||
|
||||
Press **Ctrl+C** at any time during playback to stop — PyTheory catches
|
||||
``KeyboardInterrupt`` and stops audio cleanly.
|
||||
|
||||
See :doc:`sequencing` for how to build scores and parts.
|
||||
|
||||
render_score() -- Headless Rendering
|
||||
@@ -153,7 +164,7 @@ Play a drum pattern through the speakers:
|
||||
play_pattern(Pattern.preset("rock"), repeats=4, bpm=120)
|
||||
play_pattern(Pattern.preset("bossa nova"), repeats=4, bpm=140)
|
||||
|
||||
See :doc:`drums` for the full list of 58 presets and 21 fills.
|
||||
See :doc:`drums` for the full list of 80+ presets and 21 fills.
|
||||
|
||||
play_progression() -- Quick Chord Playback
|
||||
------------------------------------------
|
||||
|
||||
@@ -667,8 +667,16 @@ A Score can use any tuning system and temperament:
|
||||
# Just intonation — pure intervals
|
||||
score = Score("4/4", bpm=90, temperament="just")
|
||||
|
||||
Temperaments: ``"equal"`` (default), ``"pythagorean"``, ``"meantone"``,
|
||||
``"just"``.
|
||||
The Score constructor accepts these tuning parameters:
|
||||
|
||||
- ``system``: Musical system name (default ``"western"``). Any system
|
||||
from :doc:`systems` works — ``"indian"``, ``"shruti"``, ``"maqam"``,
|
||||
``"carnatic"``, etc. Note strings in ``Part.add()`` are parsed against
|
||||
this system.
|
||||
- ``temperament``: Tuning temperament — ``"equal"`` (default),
|
||||
``"pythagorean"``, ``"meantone"``, ``"just"``.
|
||||
- ``reference_pitch``: Concert pitch in Hz (default 440.0). Use 415.0
|
||||
for Baroque tuning, 432.0 for "Verdi tuning", etc.
|
||||
|
||||
Custom equal temperaments via the ``TET()`` factory:
|
||||
|
||||
|
||||
+119
-10
@@ -1,7 +1,7 @@
|
||||
Synthesizers
|
||||
============
|
||||
|
||||
PyTheory includes 30 built-in waveforms and 10 ADSR envelope presets.
|
||||
PyTheory includes 41 built-in waveforms and 10 ADSR envelope presets.
|
||||
Every sound is generated from scratch -- no samples or external audio
|
||||
files needed.
|
||||
|
||||
@@ -390,11 +390,11 @@ Dedicated Instrument Synths
|
||||
--------------------------
|
||||
|
||||
Beyond the classic and physical modeling waveforms, PyTheory includes
|
||||
17 dedicated instrument synths. Each one uses tailored synthesis
|
||||
31 dedicated instrument synths. Each one uses tailored synthesis
|
||||
techniques -- additive harmonics, formant shaping, body resonance
|
||||
modeling, and specialized envelopes -- to capture the character of a
|
||||
specific acoustic instrument. These are the waveforms that bring the
|
||||
total count to 30.
|
||||
total count to 41.
|
||||
|
||||
Piano Synth
|
||||
~~~~~~~~~~~
|
||||
@@ -558,6 +558,107 @@ mids, reed buzz, and brass body warmth. Four presets: ``saxophone``,
|
||||
|
||||
sax = score.part("sax", instrument="tenor_sax")
|
||||
|
||||
Pedal Steel Synth
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Nashville crying sound — singing harmonics with slow vibrato
|
||||
and long sustain. Pairs naturally with spring reverb.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
steel = score.part("steel", instrument="pedal_steel")
|
||||
|
||||
Theremin Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Pure sine with natural hand wobble — the eerie sci-fi sound.
|
||||
Best used with legato and glide for continuous pitch.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
theremin = score.part("theremin", instrument="theremin")
|
||||
|
||||
Kalimba Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Metal tines on a wooden body. Bright, bell-like attack with
|
||||
inharmonic overtones (modes at 1x, 2.92x, 5.4x).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
kalimba = score.part("kalimba", instrument="kalimba")
|
||||
|
||||
Steel Drum Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Hammered metal pan with bright, ringing, tropical character.
|
||||
Inharmonic partials at 2.0x, 3.01x, 4.1x, 5.3x.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pan = score.part("pan", instrument="steel_drum")
|
||||
|
||||
Accordion Synth
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Musette-tuned doubled reeds — two slightly detuned reed sets
|
||||
create natural beating. Bellows pressure swell modulates amplitude.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
acc = score.part("acc", instrument="accordion")
|
||||
|
||||
Didgeridoo Synth
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Deep cylindrical drone with shifting formant overtones. The
|
||||
overtone singing effect sweeps a resonant peak between 500-1500Hz.
|
||||
Best with cave reverb.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
didg = score.part("didg", instrument="didgeridoo")
|
||||
|
||||
Bagpipe Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Bright chanter reed with constant bag pressure. All harmonics
|
||||
peaked around 3-7 (the piercing brightness). No dynamics — always ff.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pipes = score.part("pipes", instrument="bagpipe")
|
||||
|
||||
Banjo Synth
|
||||
~~~~~~~~~~~
|
||||
|
||||
Steel strings on a drum-head membrane body. The membrane gives
|
||||
nasal, ringy resonance with faster decay than guitar.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
banjo = score.part("banjo", instrument="banjo")
|
||||
|
||||
Mandolin Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Paired steel strings in 4 courses — natural chorus from the
|
||||
doubled unison strings. Bright, ringing, fast attack.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
mando = score.part("mando", instrument="mandolin")
|
||||
|
||||
Ukulele Synth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Nylon strings on a small body. Mid-heavy resonance (no deep bass),
|
||||
softer attack than guitar, shorter sustain.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
uke = score.part("uke", instrument="ukulele")
|
||||
|
||||
Granular Synth
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -614,7 +715,7 @@ Instrument Presets
|
||||
------------------
|
||||
|
||||
Instead of choosing synth + envelope + effects manually, use an
|
||||
instrument preset — 40+ predefined combinations that approximate real
|
||||
instrument preset — 60+ predefined combinations that approximate real
|
||||
instruments:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -627,20 +728,28 @@ instruments:
|
||||
|
||||
Available instruments:
|
||||
|
||||
**Keys**: piano, electric_piano, organ, harpsichord, celesta, music_box
|
||||
**Keys**: piano, electric_piano, organ, harpsichord, celesta, music_box,
|
||||
accordion
|
||||
|
||||
**Strings**: violin, viola, cello, contrabass, string_ensemble
|
||||
|
||||
**Woodwinds**: flute, clarinet, oboe, bassoon
|
||||
**Woodwinds**: flute, clarinet, oboe, bassoon, saxophone, alto_sax,
|
||||
tenor_sax, bari_sax
|
||||
|
||||
**Brass**: trumpet, trombone, french_horn, tuba, brass_ensemble
|
||||
|
||||
**Plucked**: acoustic_guitar, electric_guitar, distorted_guitar,
|
||||
bass_guitar, upright_bass, harp, sitar, koto
|
||||
**Plucked**: acoustic_guitar, electric_guitar, clean_guitar, crunch_guitar,
|
||||
distorted_guitar, orange_crunch, metal_guitar, bass_guitar, upright_bass,
|
||||
harp, sitar, koto, banjo, mandolin, mandola, ukulele
|
||||
|
||||
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass
|
||||
**World/Exotic**: pedal_steel, theremin, kalimba, steel_drum, didgeridoo,
|
||||
bagpipe
|
||||
|
||||
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells
|
||||
**Synth**: synth_lead, synth_pad, synth_bass, acid_bass, 808_bass,
|
||||
granular_pad, granular_texture, vocal, choir
|
||||
|
||||
**Percussion**: vibraphone, marimba, xylophone, glockenspiel, tubular_bells,
|
||||
timpani
|
||||
|
||||
Explicit kwargs override preset defaults:
|
||||
|
||||
|
||||
+118
-4
@@ -1,10 +1,11 @@
|
||||
Musical Systems
|
||||
===============
|
||||
|
||||
PyTheory supports **six musical systems**, each with its own tone names,
|
||||
scale patterns, and centuries of tradition behind them. Every system
|
||||
maps onto the same 12-tone equal temperament backbone, so you can
|
||||
compare scales across cultures and even combine them in your own music.
|
||||
PyTheory supports **16 musical systems** — 6 core systems mapped onto
|
||||
12-tone equal temperament, plus 10 microtonal systems with their own
|
||||
native tunings. The core systems let you compare scales across cultures;
|
||||
the microtonal systems go beyond 12-TET into genuinely different pitch
|
||||
universes.
|
||||
|
||||
Western
|
||||
-------
|
||||
@@ -271,4 +272,117 @@ produce the same pitches:
|
||||
>>> do4.frequency
|
||||
261.6255653005986
|
||||
|
||||
Microtonal Systems
|
||||
------------------
|
||||
|
||||
Beyond the six 12-TET core systems, PyTheory includes 10 microtonal
|
||||
systems that use their own native tunings — more notes per octave,
|
||||
just intonation ratios, or entirely alien pitch structures.
|
||||
|
||||
Shruti (22 tones per octave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Indian 22-shruti system divides the octave into 22 unequal steps
|
||||
using just intonation ratios. These microtonal inflections are what
|
||||
give classical Indian music its characteristic expressiveness — pitches
|
||||
that fall "between the cracks" of the piano.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=75, system="shruti")
|
||||
|
||||
Maqam (24 tones per octave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Arabic 24-tone system adds Zalzalian quarter-tone intervals
|
||||
(derived from just intonation ratios of 11 and 13) to the standard
|
||||
12 tones. These "neutral" intervals — halfway between major and minor —
|
||||
are the soul of maqam music.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=90, system="maqam")
|
||||
|
||||
Slendro (5-TET)
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The Javanese slendro scale — 5 equal divisions of the octave. Each
|
||||
step is 240 cents, wider than any Western interval. Ethereal and
|
||||
floating.
|
||||
|
||||
Pelog (9-TET)
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Approximation of the Javanese pelog tuning as 9 equal divisions of
|
||||
the octave.
|
||||
|
||||
Thai (7-TET)
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Thai classical music divides the octave into 7 equal steps of ~171
|
||||
cents each — every interval is the same size.
|
||||
|
||||
Makam (53-TET)
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Turkish makam music uses 53 equal divisions of the octave — fine
|
||||
enough to approximate virtually any just interval. The system that
|
||||
underlies Ottoman classical music.
|
||||
|
||||
Carnatic (72-TET)
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
South Indian Carnatic music theory describes 72 melakarta ragas.
|
||||
The 72-TET system provides enough resolution to represent all the
|
||||
microtonal inflections of Carnatic practice.
|
||||
|
||||
19-TET and 31-TET
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Extended equal temperaments that offer better approximations of
|
||||
just intonation intervals than 12-TET. 19-TET has excellent major
|
||||
thirds; 31-TET closely matches quarter-comma meantone.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100, system="19-tet")
|
||||
|
||||
Bohlen-Pierce (13 equal divisions of the tritave)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A genuinely alien tuning system — 13 equal divisions of the
|
||||
**tritave** (3:1 ratio) instead of the octave (2:1). No octaves, no
|
||||
fifths, built on 3:5:7 harmonics. Used by experimental composers.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
score = Score("4/4", bpm=100, system="bohlen-pierce")
|
||||
|
||||
The TET() Factory
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Create any equal temperament on the fly with the ``TET()`` factory:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import TET
|
||||
|
||||
edo19 = TET(19) # 19-tone equal temperament
|
||||
edo31 = TET(31) # 31-tone equal temperament
|
||||
score = Score("4/4", bpm=100, system=edo19)
|
||||
|
||||
Tone names in custom TET systems are integers (0, 1, 2, ..., n-1).
|
||||
|
||||
System.tone() Method
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Any system can create a Tone directly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytheory import SYSTEMS
|
||||
|
||||
western = SYSTEMS["western"]
|
||||
c4 = western.tone("C", octave=4)
|
||||
|
||||
Music is universal, but every culture hears it differently. These systems are different maps of the same territory -- explore one you've never played in before and see what you find.
|
||||
|
||||
@@ -357,6 +357,45 @@ every tone knows its enharmonic spelling:
|
||||
>>> Tone.from_string("C4", system="western").enharmonic is None
|
||||
True
|
||||
|
||||
Extended Enharmonics
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PyTheory supports the full range of enharmonic spellings used in real
|
||||
music theory:
|
||||
|
||||
- **Cb** and **Fb** — musically valid flats (Cb = B, Fb = E)
|
||||
- **E#** and **B#** — musically valid sharps (E# = F, B# = C)
|
||||
- **Double sharps** (``##`` or ``x``) — e.g. F## = G
|
||||
- **Double flats** (``bb``) — e.g. Dbb = C
|
||||
- **Unicode symbols** — ``♯`` (sharp), ``♭`` (flat), ``𝄪`` (double sharp),
|
||||
``𝄫`` (double flat) are all recognized and normalized to ASCII
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("Cb4") # resolves to B3 (octave boundary fix)
|
||||
<Tone B3>
|
||||
>>> Tone.from_string("B#4") # resolves to C5 (octave boundary fix)
|
||||
<Tone C5>
|
||||
>>> Tone.from_string("E#4") # resolves to F4
|
||||
<Tone F4>
|
||||
>>> Tone.from_string("Fb4") # resolves to E4
|
||||
<Tone E4>
|
||||
|
||||
The octave boundary is correctly handled: B# crosses up to the next
|
||||
octave (B#4 = C5), and Cb crosses down (Cb4 = B3), matching standard
|
||||
scientific pitch notation where the octave number increments at C.
|
||||
|
||||
Tone Validation
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Tones are validated on construction — if a tone name is not recognized
|
||||
in its system, a ``ValueError`` is raised:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> Tone.from_string("X4") # not a valid tone name
|
||||
ValueError: ...
|
||||
|
||||
The Circle of Fifths
|
||||
--------------------
|
||||
|
||||
|
||||
+18
-14
@@ -18,8 +18,8 @@ Theory
|
||||
------
|
||||
|
||||
The theory layer works everywhere Python runs — no audio setup needed.
|
||||
Tones, scales, chords, keys, intervals, harmony, 6 musical systems,
|
||||
25 instruments:
|
||||
Tones, scales, chords, keys, intervals, harmony, 16 musical systems,
|
||||
60+ instruments:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
@@ -72,25 +72,29 @@ every time::
|
||||
What's Inside
|
||||
-------------
|
||||
|
||||
- **Theory** — tones, scales (40+ across 6 systems), chords (17 types),
|
||||
- **Theory** — tones, scales (40+ across 16 systems), chords (17 types),
|
||||
keys, Roman numeral analysis, figured bass, pitch class sets (Forte
|
||||
numbers), scale recommendation, modulation, voice leading
|
||||
numbers), scale recommendation, modulation, voice leading, enharmonic
|
||||
support (Cb, Fb, E#, B#, double sharps/flats, unicode symbols)
|
||||
- **Sequencing** — Score, Parts, arpeggiator, legato/glide, velocity,
|
||||
swing, humanize, tempo changes, song sections with repeat
|
||||
swing, humanize, tempo changes, song sections with repeat, strumming,
|
||||
pitch bends (3 types), rolls, tuning systems (TET factory, 4
|
||||
temperaments, reference_pitch)
|
||||
- **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
|
||||
pan/spread, strumming, 80+ drum patterns (stereo panned, including world
|
||||
percussion), 21 fills
|
||||
bowed string, granular, vocal/formant, and 31 dedicated instrument synths),
|
||||
10 envelopes, 60+ instrument presets, configurable FM, sub-oscillator,
|
||||
noise layer, filter envelope, velocity-to-brightness, analog oscillator
|
||||
drift, detune, stereo pan/spread, 80+ drum patterns (stereo panned,
|
||||
including world percussion and cajón), 21 fills, 11 microtonal systems
|
||||
- **Effects** — reverb (algorithmic + 7 convolution IRs, stereo), delay,
|
||||
lowpass/highpass (with resonance), distortion, cabinet simulation,
|
||||
lowpass/highpass (with resonance), distortion, guitar cabinet simulation,
|
||||
saturation, chorus, phaser, tremolo, analog drift, sidechain compression,
|
||||
automation, LFOs. Master bus compressor/limiter
|
||||
- **Instruments** — 49 presets with fingering generation, guitar strumming,
|
||||
pitch bends
|
||||
- **Instruments** — 60+ presets with fingering generation, guitar strumming,
|
||||
pitch bends, note choking
|
||||
- **Output** — stereo playback, WAV export, MIDI import/export
|
||||
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``
|
||||
- **Interface** — REPL with tab completion, CLI (15 commands), ``pytheory demo``,
|
||||
KeyboardInterrupt handling for clean stop
|
||||
- **AI-friendly** — Claude Code can compose
|
||||
and play music through PyTheory from natural language
|
||||
|
||||
|
||||
+486
-1
@@ -1843,6 +1843,487 @@ def acoustic_ensemble():
|
||||
play_song(score, "Acoustic Ensemble — Guitar, Uke, Mandolin, Cajón")
|
||||
|
||||
|
||||
def ascent():
|
||||
"""Ascent — from the deep to the sky, theremin solo, tabla solo."""
|
||||
import random
|
||||
from pytheory import Fretboard
|
||||
random.seed(13)
|
||||
score = Score("4/4", bpm=80)
|
||||
REV = "cathedral"
|
||||
T3 = 1.0 / 12.0
|
||||
T9 = 1.0 / 9.0
|
||||
|
||||
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
|
||||
CB_ = DrumSound.CAJON_BASS; CSL_ = DrumSound.CAJON_SLAP
|
||||
CT_ = DrumSound.CAJON_TAP
|
||||
|
||||
# Didgeridoo drone
|
||||
didg = score.part("didg", instrument="didgeridoo", volume=0.15)
|
||||
for _ in range(32):
|
||||
didg.add("E1", 4.0, velocity=52)
|
||||
|
||||
# 1: THE DEEP (1-4)
|
||||
grain = score.part("grain", synth="granular_synth", envelope="pad",
|
||||
lowpass=800, reverb=0.55, reverb_type="cave", volume=0.12)
|
||||
for note in ["E2", "B2", "E2", "G2"]:
|
||||
grain.add(note, 4.0, velocity=40)
|
||||
|
||||
# 2: LIGHT (3-6)
|
||||
kal = score.part("kalimba", instrument="kalimba", volume=0.2,
|
||||
delay=0.2, delay_time=0.375, delay_feedback=0.4,
|
||||
reverb=0.45, reverb_type=REV)
|
||||
kal.rest(8.0)
|
||||
for note, vel in [("B4",58),("E5",62),("G5",65),("B5",68),
|
||||
("G5",62),("E5",58),("B4",55),("E4",52)]:
|
||||
kal.add(note, Duration.QUARTER, velocity=vel)
|
||||
kal.rest(4.0)
|
||||
for note, vel in [("E5",60),("G5",65),("B5",70),("E6",72),
|
||||
("B5",65),("G5",60),("E5",58),("B4",55)]:
|
||||
kal.add(note, Duration.QUARTER, velocity=vel)
|
||||
|
||||
# 3: SURFACING (5-8)
|
||||
cello = score.part("cello", instrument="cello", volume=0.22,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
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)
|
||||
|
||||
# 4: AIR (7-10) — piano + quiet uke
|
||||
piano = score.part("piano", instrument="piano", volume=0.28,
|
||||
reverb=0.35, reverb_type=REV)
|
||||
piano.rest(24.0)
|
||||
for notes in [
|
||||
["E3","B3","E4","G4","B4","G4","E4","B3"],
|
||||
["C3","G3","C4","E4","G4","E4","C4","G3"],
|
||||
["A2","E3","A3","C4","E4","C4","A3","E3"],
|
||||
["B2","F#3","B3","D4","F#4","D4","B3","F#3"],
|
||||
]:
|
||||
for n in notes:
|
||||
piano.add(n, Duration.EIGHTH, velocity=random.randint(58, 70))
|
||||
|
||||
fb = Fretboard.ukulele()
|
||||
uke = score.part("uke", instrument="ukulele", fretboard=fb,
|
||||
reverb=0.4, reverb_type=REV, humanize=0.2,
|
||||
pan=-0.2, volume=0.15)
|
||||
uke.rest(28.0)
|
||||
for sym in ["Em", "C", "Am", "B"]:
|
||||
vd = random.randint(60, 75)
|
||||
vu = random.randint(45, 62)
|
||||
uke.strum(sym, Duration.QUARTER, direction="down", velocity=vd)
|
||||
uke.strum(sym, Duration.EIGHTH, direction="up", velocity=vu)
|
||||
uke.strum(sym, Duration.EIGHTH, direction="down", velocity=vd - 8)
|
||||
uke.strum(sym, Duration.QUARTER, direction="up", velocity=vu)
|
||||
uke.strum(sym, Duration.QUARTER, direction="down", velocity=vd)
|
||||
|
||||
# 5: THEREMIN SOLO (11-16)
|
||||
steel = score.part("steel", instrument="pedal_steel", volume=0.16,
|
||||
reverb=0.4, reverb_type=REV, pan=0.2)
|
||||
steel.rest(36.0)
|
||||
for note, dur, vel in [("B4",3.0,58),("A4",1.0,50),("G4",2.0,55),("E4",2.0,52)]:
|
||||
steel.add(note, dur, velocity=vel)
|
||||
|
||||
theremin = score.part("theremin", instrument="theremin", volume=0.3,
|
||||
reverb=0.45, reverb_type=REV,
|
||||
delay=0.15, delay_time=0.375, delay_feedback=0.3)
|
||||
theremin.rest(40.0)
|
||||
for note, dur, vel in [
|
||||
("E4",2.0,62),("G4",1.0,58),("B4",1.0,62),
|
||||
("A4",2.0,65),("G4",1.0,58),("E4",1.0,55),("D4",3.0,60),("E4",1.0,58),
|
||||
("G4",1.0,62),("B4",1.5,68),("D5",0.5,65),
|
||||
("E5",2.0,72),("D5",1.0,65),("B4",1.0,62),
|
||||
("G4",1.0,60),("A4",1.0,62),("B4",2.0,68),
|
||||
("E5",1.5,75),("G5",1.5,80),("B5",2.0,85),
|
||||
("A5",1.0,78),("G5",1.0,72),("E5",2.0,75),
|
||||
("D5",1.0,68),("B4",1.0,62),("E4",4.0,70),
|
||||
]:
|
||||
theremin.add(note, dur, velocity=vel)
|
||||
|
||||
strings = score.part("strings", instrument="string_ensemble", volume=0.15,
|
||||
reverb=0.45, reverb_type=REV)
|
||||
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)
|
||||
|
||||
# 6: THE PEAK (17-18)
|
||||
flute = score.part("flute", instrument="flute", volume=0.2, reverb=0.4, reverb_type=REV)
|
||||
flute.rest(64.0)
|
||||
for note, dur, vel in [
|
||||
("B5",2.0,55),("A5",1.0,50),("G5",1.0,52),
|
||||
("E5",2.0,55),("D5",1.0,50),("E5",1.0,52),
|
||||
]:
|
||||
flute.add(note, dur, velocity=vel)
|
||||
|
||||
harp = score.part("harp", instrument="harp", volume=0.16, reverb=0.4, reverb_type=REV)
|
||||
harp.rest(68.0)
|
||||
for n in ["B5","G5","E5","B4","G4","E4","B3","E3"]:
|
||||
harp.add(n, Duration.QUARTER, velocity=random.randint(48, 58))
|
||||
|
||||
timp = score.part("timp", instrument="timpani")
|
||||
timp.rest(64.0)
|
||||
timp.roll("E2", 4.0, velocity_start=20, velocity_end=95, speed=0.125)
|
||||
timp.add("E2", 4.0, velocity=105)
|
||||
|
||||
# Drums: silence → cajón → djembe → tabla solo
|
||||
score.add_pattern(Pattern(name="s", time_signature="4/4", beats=16.0, hits=[]),
|
||||
repeats=1)
|
||||
p_caj = Pattern(name="caj", time_signature="4/4", beats=4.0, hits=[
|
||||
_Hit(CB_, 0.0, 58), _Hit(CT_, 0.5, 22), _Hit(CSL_, 1.0, 50),
|
||||
_Hit(CT_, 1.5, 20), _Hit(CB_, 2.0, 55), _Hit(CT_, 2.5, 22),
|
||||
_Hit(CSL_, 3.0, 52),
|
||||
])
|
||||
score.add_pattern(p_caj, repeats=4)
|
||||
p_dj = Pattern(name="dj", time_signature="4/4", beats=4.0, hits=[
|
||||
_Hit(DJB_, 0.0, 42), _Hit(DJT_, 1.0, 35), _Hit(DJT_, 1.5, 30),
|
||||
_Hit(DJS_, 2.0, 38), _Hit(DJT_, 3.0, 35),
|
||||
])
|
||||
score.add_pattern(p_dj, repeats=10)
|
||||
|
||||
# 7: TABLA SOLO (bars 19-26)
|
||||
score.add_pattern(Pattern(name="ts1", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DH_, 0.0, 72), _Hit(NA, 2.5, 48),
|
||||
_Hit(DH_, 4.0, 75), _Hit(TT_, 5.5, 25), _Hit(NA, 6.0, 45),
|
||||
_Hit(DH_, 7.5, 72),
|
||||
]), repeats=1)
|
||||
score.add_pattern(Pattern(name="ts2", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DH_, 0.0, 85), _Hit(TT_, 0.25, 30), _Hit(TT_, 0.5, 32),
|
||||
_Hit(NA, 1.0, 62), _Hit(TT_, 1.25, 25), _Hit(NA, 2.0, 58),
|
||||
_Hit(TT_, 2.5, 28), _Hit(DH_, 3.0, 82),
|
||||
_Hit(DH_, 4.0, 90), _Hit(TT_, 4.25, 32), _Hit(TT_, 4.5, 35),
|
||||
_Hit(NA, 5.0, 68), _Hit(KE_, 5.5, 35), _Hit(NA, 6.0, 62),
|
||||
_Hit(KE_, 6.5, 38), _Hit(DH_, 7.0, 92), _Hit(GB_, 7.5, 85),
|
||||
]), repeats=1)
|
||||
score.add_pattern(Pattern(name="ts3", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(NA, 0.0, 108), _Hit(NA, 0.25, 52), _Hit(TT_, 0.5, 35),
|
||||
_Hit(NA, 0.75, 100),
|
||||
_Hit(GE_, 1.0, 98), _Hit(GE_, 1.25, 48), _Hit(GB_, 1.5, 90),
|
||||
_Hit(GE_, 1.75, 42),
|
||||
_Hit(NA, 2.0, 110), _Hit(TT_, 2.125, 28), _Hit(TT_, 2.25, 32),
|
||||
_Hit(NA, 2.5, 102), _Hit(TT_, 2.625, 30), _Hit(TT_, 2.75, 35),
|
||||
_Hit(GB_, 3.0, 110), _Hit(KE_, 3.25, 45), _Hit(GE_, 3.5, 65),
|
||||
_Hit(DH_, 4.0, 112),
|
||||
*[_Hit(TT_ if i % 2 == 0 else KE_, 5.0 + i * T9, 35 + i * 5)
|
||||
for i in range(9)],
|
||||
_Hit(DH_, 7.0, 115),
|
||||
]), repeats=1)
|
||||
score.add_pattern(Pattern(name="ts4", time_signature="4/4", beats=8.0, hits=[
|
||||
*[_Hit(TT_, i * T3, 32 + i * 2) for i in range(12)],
|
||||
_Hit(DH_, 1.0, 115), _Hit(GB_, 1.5, 105),
|
||||
_Hit(NA, 2.0, 108), _Hit(KE_, 2.125, 42), _Hit(NA, 2.25, 102),
|
||||
_Hit(KE_, 2.375, 45), _Hit(NA, 2.5, 105), _Hit(KE_, 2.625, 48),
|
||||
_Hit(NA, 2.75, 110), _Hit(DH_, 3.0, 118),
|
||||
*[_Hit(TT_, 3.5 + i * T3, 30 + i * 4) for i in range(12)],
|
||||
_Hit(DH_, 4.5, 120), _Hit(DH_, 4.75, 115), _Hit(GB_, 5.0, 112),
|
||||
_Hit(GE_, 5.5, 85), _Hit(GE_, 6.5, 82),
|
||||
*[_Hit(NA if i % 3 == 0 else TT_, 5.5 + i * (2.0 / 9.0),
|
||||
40 + (i % 3) * 12) for i in range(9)],
|
||||
_Hit(DH_, 7.5, 127), _Hit(GB_, 7.875, 127),
|
||||
]), repeats=1)
|
||||
score.set_drum_effects(reverb=0.3, reverb_type=REV)
|
||||
|
||||
play_song(score, "Ascent — Deep → Sky → Theremin Solo → Tabla Solo")
|
||||
|
||||
|
||||
def descent():
|
||||
"""Descent — generative, different every time. From sky to deep."""
|
||||
import random
|
||||
import time
|
||||
from pytheory import Fretboard, TonedScale
|
||||
|
||||
# No seed — truly random every play
|
||||
random.seed(int(time.time() * 1000) % 2**31)
|
||||
|
||||
# Random key — always minor, always dark
|
||||
roots = ["A", "B", "C", "D", "E", "F", "G"]
|
||||
root = random.choice(roots)
|
||||
mode = random.choice(["minor", "harmonic minor"])
|
||||
key = Key(root, mode)
|
||||
scale_tones = [t.name for t in key.scale.tones[:-1]]
|
||||
|
||||
bpm = random.randint(65, 85)
|
||||
score = Score("4/4", bpm=bpm)
|
||||
REV = random.choice(["cathedral", "taj_mahal", "cave"])
|
||||
|
||||
T3 = 1.0 / 12.0
|
||||
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
|
||||
|
||||
print(f" {root} {mode} | {bpm} bpm | {REV}")
|
||||
|
||||
# ── Pad drone — random synth, the whole piece ──
|
||||
pad_synth = random.choice(["strings_synth", "granular_synth", "vocal_synth"])
|
||||
pad = score.part("pad", synth=pad_synth, envelope="pad",
|
||||
detune=random.randint(6, 14), spread=0.4,
|
||||
reverb=0.5, reverb_type=REV, volume=0.15,
|
||||
analog=random.uniform(0.1, 0.4))
|
||||
prog = key.progression("i", "iv", "V", "i")
|
||||
for _ in range(6):
|
||||
for chord in prog:
|
||||
pad.add(chord, Duration.WHOLE, velocity=random.randint(48, 62))
|
||||
|
||||
# ── 1: HIGH — theremin or flute, random melody from scale ──
|
||||
lead_synth = random.choice(["theremin", "flute", "pedal_steel"])
|
||||
lead = score.part("lead", instrument=lead_synth, volume=0.25,
|
||||
reverb=0.4, reverb_type=REV,
|
||||
delay=0.2, delay_time=random.uniform(0.2, 0.5),
|
||||
delay_feedback=random.uniform(0.2, 0.4))
|
||||
lead.rest(4.0)
|
||||
# Generate melody by WALKING the scale — stepwise with occasional leaps
|
||||
# Real melodies move to neighboring notes, not random jumps
|
||||
scale_idx = len(scale_tones) - 1 # start high
|
||||
octave = 5
|
||||
for _ in range(random.randint(10, 16)):
|
||||
note = scale_tones[scale_idx]
|
||||
dur = random.choice([1.5, 2.0, 3.0, 4.0])
|
||||
vel = random.randint(58, 70)
|
||||
lead.add(f"{note}{octave}", dur, velocity=vel)
|
||||
# Mostly step down (descent!), sometimes hold, rare leap
|
||||
r = random.random()
|
||||
if r < 0.5: # step down
|
||||
scale_idx -= 1
|
||||
elif r < 0.65: # step up (tension)
|
||||
scale_idx += 1
|
||||
elif r < 0.8: # leap down
|
||||
scale_idx -= random.randint(2, 3)
|
||||
# else hold same note
|
||||
# Wrap octaves
|
||||
if scale_idx < 0:
|
||||
scale_idx += len(scale_tones)
|
||||
octave -= 1
|
||||
elif scale_idx >= len(scale_tones):
|
||||
scale_idx -= len(scale_tones)
|
||||
octave += 1
|
||||
octave = max(3, min(5, octave))
|
||||
|
||||
# ── 2: Kalimba or steel drum — random arpeggios ──
|
||||
sparkle_inst = random.choice(["kalimba", "steel_drum", "vibraphone", "harp"])
|
||||
sparkle = score.part("sparkle", instrument=sparkle_inst, volume=0.18,
|
||||
delay=0.2, delay_time=random.uniform(0.15, 0.4),
|
||||
delay_feedback=random.uniform(0.3, 0.5),
|
||||
reverb=0.4, reverb_type=REV)
|
||||
sparkle.rest(8.0)
|
||||
# Arpeggios — walk chord tones in patterns, not random
|
||||
for chord in prog * 3:
|
||||
chord_tones = [t.name for t in chord.tones]
|
||||
pattern_type = random.choice(["up", "down", "updown"])
|
||||
oct = random.choice([4, 5])
|
||||
if pattern_type == "up":
|
||||
seq = chord_tones + [chord_tones[0]]
|
||||
elif pattern_type == "down":
|
||||
seq = list(reversed(chord_tones)) + [chord_tones[-1]]
|
||||
else:
|
||||
seq = chord_tones + list(reversed(chord_tones[1:-1]))
|
||||
# Pad to 8 notes
|
||||
while len(seq) < 8:
|
||||
seq = seq + seq
|
||||
seq = seq[:8]
|
||||
for i, n in enumerate(seq):
|
||||
o = oct + (1 if i >= 4 and pattern_type == "up" else 0)
|
||||
sparkle.add(f"{n}{o}", Duration.EIGHTH,
|
||||
velocity=random.randint(48, 58))
|
||||
|
||||
# ── 3: Piano — random broken chords ──
|
||||
piano = score.part("piano", instrument="piano", volume=0.22,
|
||||
reverb=0.35, reverb_type=REV)
|
||||
piano.rest(random.uniform(8.0, 16.0))
|
||||
for chord in prog * 2:
|
||||
chord_tones = [t.name for t in chord.tones]
|
||||
oct = random.choice([3, 4])
|
||||
# Walk up the chord then back down
|
||||
up = [f"{n}{oct}" for n in chord_tones]
|
||||
up.append(f"{chord_tones[0]}{oct+1}")
|
||||
down = [f"{n}{oct}" for n in reversed(chord_tones[:-1])]
|
||||
arp = (up + down)[:8]
|
||||
for n in arp:
|
||||
piano.add(n, Duration.EIGHTH,
|
||||
velocity=random.randint(58, 65))
|
||||
|
||||
# ── 4: Cello — descending long notes ──
|
||||
cello = score.part("cello", instrument="cello", volume=0.2,
|
||||
reverb=0.4, reverb_type=REV)
|
||||
cello.rest(16.0)
|
||||
oct = 3
|
||||
for _ in range(8):
|
||||
note = random.choice(scale_tones)
|
||||
cello.add(f"{note}{oct}", 4.0, velocity=random.randint(48, 62))
|
||||
if random.random() < 0.4 and oct > 2:
|
||||
oct -= 1
|
||||
|
||||
# ── 5: Bass — deep, sparse ──
|
||||
bass_inst = random.choice(["upright_bass", "didgeridoo"])
|
||||
bass = score.part("bass", instrument=bass_inst, volume=0.18)
|
||||
bass.rest(random.uniform(4.0, 12.0))
|
||||
for chord in prog * 4:
|
||||
r = chord.root
|
||||
if r:
|
||||
oct = 2 if bass_inst == "upright_bass" else 1
|
||||
bass.add(f"{r.name}{oct}", 4.0,
|
||||
velocity=random.randint(50, 65))
|
||||
|
||||
# ── Drums: random combination ──
|
||||
drum_start = random.randint(2, 5) * 4 # 8-20 beats silence
|
||||
score.add_pattern(Pattern(name="s", time_signature="4/4",
|
||||
beats=float(drum_start), hits=[]),
|
||||
repeats=1)
|
||||
|
||||
# Pick random world drum combo
|
||||
drum_choice = random.choice(["cajon folk", "djembe", "keherwa", "dadra"])
|
||||
score.drums(drum_choice, repeats=random.randint(6, 12))
|
||||
|
||||
# Tabla solo at the end — always
|
||||
score.add_pattern(Pattern(name="ts1", time_signature="4/4", beats=8.0, hits=[
|
||||
_Hit(DH_, 0.0, 72), _Hit(NA, 2.5, 48),
|
||||
_Hit(DH_, 4.0, 75), _Hit(TT_, 5.5, 25), _Hit(NA, 6.0, 45),
|
||||
_Hit(DH_, 7.5, 72),
|
||||
]), repeats=1)
|
||||
|
||||
# Generate random tabla solo — different every time
|
||||
solo_hits = []
|
||||
beat = 0.0
|
||||
while beat < 16.0:
|
||||
stroke = random.choice([DH_, NA, TT_, KE_, GB_, GE_])
|
||||
vel = random.randint(35, 120)
|
||||
# Ghost notes are quiet, accents are loud
|
||||
if stroke == TT_:
|
||||
vel = random.randint(25, 50)
|
||||
elif stroke in (DH_, GB_):
|
||||
vel = random.randint(70, 120)
|
||||
solo_hits.append(_Hit(stroke, beat, vel))
|
||||
# Random spacing — mix of tight and open
|
||||
beat += random.choice([0.125, 0.25, 0.25, 0.5, 0.5, 1.0])
|
||||
|
||||
score.add_pattern(Pattern(name="ts_rand", time_signature="4/4",
|
||||
beats=16.0, hits=solo_hits), repeats=1)
|
||||
score.set_drum_effects(reverb=0.3, reverb_type=REV)
|
||||
|
||||
play_song(score, f"Descent — {root} {mode} (generative, {REV})")
|
||||
|
||||
|
||||
def pop_rock():
|
||||
"""Pop Rock — the I-V-vi-IV progression that launched a thousand hits."""
|
||||
import random
|
||||
from pytheory import Fretboard
|
||||
random.seed(42)
|
||||
score = Score("4/4", bpm=120)
|
||||
fb = Fretboard.guitar()
|
||||
|
||||
guitar = score.part("guitar", instrument="acoustic_guitar", fretboard=fb,
|
||||
reverb=0.2, reverb_type="plate", humanize=0.15, pan=-0.2)
|
||||
bass = score.part("bass", instrument="bass_guitar", volume=0.35, humanize=0.1)
|
||||
lead = score.part("lead", instrument="electric_guitar",
|
||||
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")
|
||||
|
||||
prog = ["G", "D", "Em", "C"]
|
||||
|
||||
# Intro — picked
|
||||
for sym in prog:
|
||||
chord_obj = Chord.from_symbol(sym)
|
||||
tones = [t.name for t in chord_obj.tones]
|
||||
for t in tones:
|
||||
guitar.add(f"{t}3", Duration.QUARTER,
|
||||
velocity=random.randint(62, 72))
|
||||
|
||||
# Verse — strum
|
||||
for _ in range(2):
|
||||
for sym in prog:
|
||||
vd = random.randint(72, 88)
|
||||
vu = random.randint(55, 70)
|
||||
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)
|
||||
|
||||
bass_notes = ["G2", "G2", "D2", "D2", "E2", "E2", "C2", "C2"]
|
||||
for _ in range(3):
|
||||
for n in bass_notes:
|
||||
bass.add(n, Duration.HALF, velocity=random.randint(68, 78))
|
||||
|
||||
# Melody
|
||||
for _ in range(4):
|
||||
lead.rest(Duration.WHOLE)
|
||||
for note, dur, vel in [
|
||||
("B4",0.5,78),("B4",0.5,75),("B4",0.5,78),("B4",0.5,72),
|
||||
("A4",0.5,75),("A4",0.5,72),("B4",1.0,80),
|
||||
("B4",0.5,78),("B4",0.5,75),("B4",0.5,78),("D5",0.5,82),
|
||||
("G4",0.75,72),("G4",0.25,68),("A4",1.0,78),
|
||||
("B4",0.5,78),("B4",0.5,75),("B4",0.5,78),("B4",0.5,72),
|
||||
("A4",0.5,75),("A4",0.5,72),("B4",0.5,78),("A4",0.5,72),
|
||||
("G4",2.0,80),
|
||||
]:
|
||||
lead.add(note, dur, velocity=vel)
|
||||
|
||||
for _ in range(4):
|
||||
strings.rest(Duration.WHOLE)
|
||||
for sym in prog * 2:
|
||||
strings.add(Chord.from_symbol(sym), Duration.WHOLE,
|
||||
velocity=random.randint(55, 68))
|
||||
|
||||
score.drums("rock", repeats=6)
|
||||
|
||||
play_song(score, "Pop Rock — G D Em C (I-V-vi-IV)")
|
||||
|
||||
|
||||
def sitar_drone():
|
||||
"""Sitar Drone — Raga Bhairav with hold() polyphony, 22-shruti JI."""
|
||||
shruti = SYSTEMS["shruti"]
|
||||
score = Score("4/4", bpm=72, system=shruti)
|
||||
|
||||
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
|
||||
|
||||
sitar = score.part("sitar", instrument="sitar", volume=0.3,
|
||||
reverb=0.4, reverb_type="taj_mahal")
|
||||
# Sa drone held — rings under the whole melody
|
||||
sitar.hold(Tone("Sa", octave=3, system=shruti), 32.0, velocity=60)
|
||||
sitar.rest(Duration.WHOLE)
|
||||
for tone, dur, vel in [
|
||||
(S, 2.0, 72), (kR, 0.5, 62), (S, 0.5, 68),
|
||||
(G, 2.0, 78), (kR, 0.5, 60), (G, 0.5, 70),
|
||||
(M, 1.5, 75), (P, 2.5, 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, 85),
|
||||
(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, 4.0, 80),
|
||||
]:
|
||||
sitar.add(tone, dur, velocity=vel)
|
||||
|
||||
tanpura = score.part("tanpura", synth="strings_synth", envelope="pad",
|
||||
detune=3, lowpass=900, volume=0.12,
|
||||
reverb=0.5, reverb_type="taj_mahal")
|
||||
tanpura_pa = score.part("tanpura_pa", synth="strings_synth", envelope="pad",
|
||||
detune=3, lowpass=1200, volume=0.1,
|
||||
reverb=0.5, reverb_type="taj_mahal")
|
||||
for _ in range(8):
|
||||
tanpura.add(Tone("Sa", octave=3, system=shruti), Duration.WHOLE)
|
||||
tanpura_pa.add(Tone("Pa", octave=3, system=shruti), Duration.WHOLE)
|
||||
|
||||
NA = DrumSound.TABLA_NA
|
||||
DH_ = DrumSound.TABLA_DHA
|
||||
TT_ = DrumSound.TABLA_TIT
|
||||
silence = Pattern(name="s", time_signature="4/4", beats=8.0, hits=[])
|
||||
score.add_pattern(silence, repeats=1)
|
||||
p = Pattern(name="t", time_signature="4/4", beats=4.0, hits=[
|
||||
_Hit(DH_, 0.0, 68), _Hit(TT_, 0.5, 25), _Hit(NA, 1.0, 55),
|
||||
_Hit(NA, 2.0, 52), _Hit(DH_, 3.0, 68),
|
||||
])
|
||||
score.add_pattern(p, repeats=6)
|
||||
score.set_drum_effects(reverb=0.25, reverb_type="taj_mahal")
|
||||
|
||||
play_song(score, "Sitar Drone — Raga Bhairav (22-Shruti JI, hold() polyphony)")
|
||||
|
||||
|
||||
SONGS = {
|
||||
"1": ("Bossa Nova in A minor", bossa_nova_girl),
|
||||
"2": ("Bebop in Bb major", bebop_in_bb),
|
||||
@@ -1870,6 +2351,10 @@ SONGS = {
|
||||
"24": ("Journey (Western → World → Indian)", journey),
|
||||
"25": ("Epic Bhairav (Orchestral + Tabla)", epic_bhairav),
|
||||
"26": ("Acoustic Ensemble (Guitar+Uke+Mando+Cajón)", acoustic_ensemble),
|
||||
"27": ("Ascent (Deep → Sky → Tabla Solo)", ascent),
|
||||
"28": ("Descent (Generative — different every time)", descent),
|
||||
"29": ("Pop Rock (I-V-vi-IV)", pop_rock),
|
||||
"30": ("Sitar Drone (Bhairav, hold() polyphony)", sitar_drone),
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -1883,7 +2368,7 @@ if __name__ == "__main__":
|
||||
print(f" {key:>2}. {name}")
|
||||
|
||||
print()
|
||||
choice = input(" Pick a song (1-26, or 'all'): ").strip()
|
||||
choice = input(" Pick a song (1-30, or 'all'): ").strip()
|
||||
print()
|
||||
|
||||
if choice == "all":
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pytheory"
|
||||
version = "0.36.2"
|
||||
version = "0.36.6"
|
||||
description = "Music Theory for Humans"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""PyTheory: Music Theory for Humans."""
|
||||
|
||||
__version__ = "0.36.2"
|
||||
__version__ = "0.36.6"
|
||||
|
||||
from .tones import Tone, Interval
|
||||
from .systems import System, SYSTEMS, TET
|
||||
|
||||
+102
-3
@@ -1237,6 +1237,47 @@ def steel_drum_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
return (peak * wave).astype(numpy.int16)
|
||||
|
||||
|
||||
def harmonium_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE):
|
||||
"""Harmonium — Indian pump organ, single free reed per note.
|
||||
|
||||
Unlike accordion (doubled musette reeds), the harmonium has one
|
||||
reed per note — no beating, just a pure, nasal, reedy tone.
|
||||
Constant bellows pressure, warm but slightly buzzy. The sound
|
||||
of kirtan, qawwali, and devotional music.
|
||||
"""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float64) / SAMPLE_RATE
|
||||
rng = numpy.random.default_rng(int(hz * 100) % 2**31)
|
||||
|
||||
# Single reed — odd harmonics stronger (like clarinet but warmer)
|
||||
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) * (1.0 if n % 2 == 1 else 0.5)
|
||||
phase = rng.uniform(0, 2 * numpy.pi)
|
||||
wave += amp * numpy.sin(2 * numpy.pi * f_n * t + phase)
|
||||
|
||||
# Bellows pressure — gentle swell, slower than accordion
|
||||
bellows = 0.9 + 0.1 * numpy.sin(2 * numpy.pi * 0.5 * t)
|
||||
wave *= bellows
|
||||
|
||||
# Nasal character — slight midrange boost
|
||||
import scipy.signal as _sig
|
||||
center = min(1200, hz * 3)
|
||||
lo = max(20, int(center - 300))
|
||||
hi = min(SAMPLE_RATE // 2 - 1, int(center + 300))
|
||||
if lo < hi:
|
||||
bp, ap = _sig.butter(2, [lo, hi], btype='band', fs=SAMPLE_RATE)
|
||||
nasal = _sig.lfilter(bp, ap, wave) * 0.2
|
||||
wave += nasal
|
||||
|
||||
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.
|
||||
|
||||
@@ -1794,6 +1835,7 @@ class Synth(Enum):
|
||||
THEREMIN = "theremin_synth"
|
||||
KALIMBA = "kalimba_synth"
|
||||
STEEL_DRUM = "steel_drum_synth"
|
||||
HARMONIUM = "harmonium_synth"
|
||||
ACCORDION = "accordion_synth"
|
||||
DIDGERIDOO = "didgeridoo_synth"
|
||||
BAGPIPE = "bagpipe_synth"
|
||||
@@ -1825,7 +1867,7 @@ _SYNTH_FUNCTIONS = {
|
||||
"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,
|
||||
"harmonium_synth": harmonium_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,
|
||||
@@ -2584,6 +2626,52 @@ def _synth_mridangam_tha(n_samples):
|
||||
return out
|
||||
|
||||
|
||||
def _synth_doumbek_dum(n_samples):
|
||||
"""Doumbek Dum — open center strike, deep and round."""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
freq = 80 + 40 * numpy.exp(-25 * t)
|
||||
phase = 2 * numpy.pi * numpy.cumsum(freq) / SAMPLE_RATE
|
||||
body = numpy.sin(phase) * _exp_decay(n_samples, 8) * 0.8
|
||||
thump_len = min(int(SAMPLE_RATE * 0.04), n_samples)
|
||||
import scipy.signal as _sig
|
||||
thump = _noise(thump_len)
|
||||
if thump_len > 20:
|
||||
bl, al = _sig.butter(2, [50, 250], btype='band', fs=SAMPLE_RATE)
|
||||
thump = _sig.lfilter(bl, al, numpy.pad(thump, (0, max(0, n_samples - thump_len))))[:thump_len].astype(numpy.float32)
|
||||
thump *= _exp_decay(thump_len, 22) * 0.7
|
||||
body[:thump_len] += thump
|
||||
return numpy.tanh(body * 1.3).astype(numpy.float32)
|
||||
|
||||
|
||||
def _synth_doumbek_tek(n_samples):
|
||||
"""Doumbek Tek — sharp edge strike, bright and cutting."""
|
||||
t = numpy.arange(n_samples, dtype=numpy.float32) / SAMPLE_RATE
|
||||
ring = numpy.sin(2 * numpy.pi * 400 * t) * _exp_decay(n_samples, 22) * 0.5
|
||||
ring2 = numpy.sin(2 * numpy.pi * 900 * t) * 0.3 * _exp_decay(n_samples, 30)
|
||||
click_len = min(int(SAMPLE_RATE * 0.005), n_samples)
|
||||
click = _noise(click_len) * _exp_decay(click_len, 300) * 0.9
|
||||
import scipy.signal as _sig
|
||||
if click_len > 10:
|
||||
bl, al = _sig.butter(2, [2000, min(8000, SAMPLE_RATE // 2 - 1)], btype='band', fs=SAMPLE_RATE)
|
||||
click = _sig.lfilter(bl, al, numpy.pad(click, (0, max(0, n_samples - click_len))))[:click_len].astype(numpy.float32)
|
||||
result = ring + ring2
|
||||
result[:click_len] += click
|
||||
return numpy.tanh(result * 1.8).astype(numpy.float32)
|
||||
|
||||
|
||||
def _synth_doumbek_ka(n_samples):
|
||||
"""Doumbek Ka — muted edge slap, short and dry."""
|
||||
n = min(n_samples, int(SAMPLE_RATE * 0.04))
|
||||
t = numpy.arange(n, dtype=numpy.float32) / SAMPLE_RATE
|
||||
body = numpy.sin(2 * numpy.pi * 350 * t) * _exp_decay(n, 30) * 0.4
|
||||
slap = _noise(min(80, n)) * _exp_decay(min(80, n), 200) * 0.7
|
||||
result = body
|
||||
result[:min(80, n)] += slap
|
||||
out = numpy.zeros(n_samples, dtype=numpy.float32)
|
||||
out[:n] = numpy.tanh(result * 1.5)
|
||||
return out
|
||||
|
||||
|
||||
def _synth_cajon_bass(n_samples):
|
||||
"""Cajón bass — palm strike on center of the face.
|
||||
|
||||
@@ -2914,6 +3002,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),
|
||||
# Doumbek
|
||||
DrumSound.DOUMBEK_DUM.value: lambda n: _synth_doumbek_dum(n),
|
||||
DrumSound.DOUMBEK_TEK.value: lambda n: _synth_doumbek_tek(n),
|
||||
DrumSound.DOUMBEK_KA.value: lambda n: _synth_doumbek_ka(n),
|
||||
# Cajon
|
||||
DrumSound.CAJON_BASS.value: lambda n: _synth_cajon_bass(n),
|
||||
DrumSound.CAJON_SLAP.value: lambda n: _synth_cajon_slap(n),
|
||||
@@ -4200,7 +4292,9 @@ def _render_notes_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
# Right channel gets up-detuned, left gets down-detuned
|
||||
stereo_buf[start:end, 1] += up_env * gain * spread_amt
|
||||
stereo_buf[start:end, 0] += down_env * gain * spread_amt
|
||||
beat_pos += note.beats
|
||||
# hold() notes don't advance the beat position
|
||||
if not getattr(note, '_hold', False):
|
||||
beat_pos += note.beats
|
||||
|
||||
|
||||
def _render_legato_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
@@ -4242,7 +4336,8 @@ def _render_legato_to_buf(notes, buf, samples_per_beat, total_samples,
|
||||
events.append((start, end, hz, vel))
|
||||
else:
|
||||
events.append((start, end, 0, vel)) # rest
|
||||
beat_pos += note.beats
|
||||
if not getattr(note, '_hold', False):
|
||||
beat_pos += note.beats
|
||||
|
||||
if not events:
|
||||
return
|
||||
@@ -4507,6 +4602,10 @@ def render_score(score):
|
||||
DrumSound.DJEMBE_BASS.value: 0.0,
|
||||
DrumSound.DJEMBE_TONE.value: 0.1,
|
||||
DrumSound.DJEMBE_SLAP.value: -0.1,
|
||||
# Doumbek
|
||||
DrumSound.DOUMBEK_DUM.value: 0.0,
|
||||
DrumSound.DOUMBEK_TEK.value: 0.1,
|
||||
DrumSound.DOUMBEK_KA.value: -0.1,
|
||||
# Cajon — centered (single instrument)
|
||||
DrumSound.CAJON_BASS.value: 0.0,
|
||||
DrumSound.CAJON_SLAP.value: 0.0,
|
||||
|
||||
+339
-14
@@ -213,6 +213,11 @@ INSTRUMENTS = {
|
||||
"synth": "steel_drum_synth", "envelope": "none",
|
||||
"reverb": 0.3, "reverb_type": "plate",
|
||||
},
|
||||
"harmonium": {
|
||||
"synth": "harmonium_synth", "envelope": "organ",
|
||||
"reverb": 0.2, "reverb_type": "taj_mahal",
|
||||
"humanize": 0.15,
|
||||
},
|
||||
"accordion": {
|
||||
"synth": "accordion_synth", "envelope": "organ",
|
||||
"humanize": 0.15,
|
||||
@@ -383,6 +388,24 @@ class Duration(Enum):
|
||||
DOTTED_QUARTER = 1.5
|
||||
TRIPLET_QUARTER = 2 / 3
|
||||
|
||||
# Arithmetic — lets you write ``Duration.WHOLE * 2`` → 8.0 beats.
|
||||
def __mul__(self, other):
|
||||
return self.value * other
|
||||
|
||||
def __rmul__(self, other):
|
||||
return self.value * other
|
||||
|
||||
def __truediv__(self, other):
|
||||
return self.value / other
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, Duration):
|
||||
return self.value + other.value
|
||||
return self.value + other
|
||||
|
||||
def __radd__(self, other):
|
||||
return other + self.value
|
||||
|
||||
|
||||
class TimeSignature:
|
||||
"""A musical time signature like 4/4 or 6/8."""
|
||||
@@ -426,6 +449,7 @@ class Note:
|
||||
bend: float = 0.0
|
||||
bend_type: str = "smooth" # "smooth" (log), "linear", "late"
|
||||
lyric: str = "" # syllable for vocal synth
|
||||
_hold: bool = False # if True, don't advance beat position
|
||||
|
||||
@property
|
||||
def beats(self) -> float:
|
||||
@@ -527,6 +551,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)
|
||||
# Doumbek (darbuka) sounds
|
||||
DOUMBEK_DUM = 112 # center of head, deep bass
|
||||
DOUMBEK_TEK = 113 # edge of head, sharp high
|
||||
DOUMBEK_KA = 114 # muted edge slap
|
||||
# Cajon sounds
|
||||
CAJON_BASS = 108 # center of face, deep thump
|
||||
CAJON_SLAP = 109 # top edge, snare wires buzz
|
||||
@@ -1562,6 +1590,61 @@ Pattern._PRESETS["tabla solo"] = dict(
|
||||
],
|
||||
)
|
||||
|
||||
# ── Doumbek patterns ──────────────────────────────────────────────────────
|
||||
DKD = DrumSound.DOUMBEK_DUM
|
||||
DKT = DrumSound.DOUMBEK_TEK
|
||||
DKK = DrumSound.DOUMBEK_KA
|
||||
|
||||
# Maqsoum — the most common Arabic rhythm (4/4)
|
||||
Pattern._PRESETS["maqsoum"] = dict(
|
||||
name="maqsoum",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(DKD, 0.0, 85), _h(DKT, 0.5, 65),
|
||||
_h(DKT, 1.0, 68), _h(DKD, 1.5, 80),
|
||||
_h(DKT, 2.0, 65), _h(DKT, 2.5, 62),
|
||||
_h(DKT, 3.0, 68), _h(DKT, 3.5, 62),
|
||||
],
|
||||
)
|
||||
|
||||
# Baladi — heavy, earthy, belly dance
|
||||
Pattern._PRESETS["baladi"] = dict(
|
||||
name="baladi",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(DKD, 0.0, 88), _h(DKD, 0.5, 78),
|
||||
_h(DKT, 1.0, 70), _h(DKD, 1.5, 82),
|
||||
_h(DKT, 2.0, 68), _h(DKT, 2.5, 62),
|
||||
_h(DKT, 3.0, 68), _h(DKK, 3.25, 45), _h(DKT, 3.5, 65),
|
||||
],
|
||||
)
|
||||
|
||||
# Saidi — Upper Egyptian, strong and driving
|
||||
Pattern._PRESETS["saidi"] = dict(
|
||||
name="saidi",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(DKD, 0.0, 88), _h(DKT, 0.5, 65),
|
||||
_h(DKD, 1.0, 82), _h(DKD, 1.5, 78),
|
||||
_h(DKT, 2.0, 70), _h(DKT, 2.5, 62),
|
||||
_h(DKT, 3.0, 68), _h(DKT, 3.5, 62),
|
||||
],
|
||||
)
|
||||
|
||||
# Ayoub — simple 2/4, trance-like repetition
|
||||
Pattern._PRESETS["ayoub"] = dict(
|
||||
name="ayoub",
|
||||
time_signature="2/4",
|
||||
beats=2.0,
|
||||
hits=[
|
||||
_h(DKD, 0.0, 85), _h(DKK, 0.5, 45),
|
||||
_h(DKT, 1.0, 70), _h(DKT, 1.5, 62),
|
||||
],
|
||||
)
|
||||
|
||||
# ── Cajón patterns ────────────────────────────────────────────────────────
|
||||
CB = DrumSound.CAJON_BASS
|
||||
CSL = DrumSound.CAJON_SLAP
|
||||
@@ -2092,6 +2175,226 @@ Pattern._FILLS["second line"] = dict(
|
||||
],
|
||||
)
|
||||
|
||||
# ── Doumbek fills ────────────────────────────────────────────────────────
|
||||
_DKD = DrumSound.DOUMBEK_DUM
|
||||
_DKT = DrumSound.DOUMBEK_TEK
|
||||
_DKK = DrumSound.DOUMBEK_KA
|
||||
|
||||
# Doumbek roll — rapid teks building to dum
|
||||
Pattern._FILLS["doumbek roll"] = dict(
|
||||
name="doumbek roll fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
*[_h(_DKT, i * 0.125, 40 + i * 4) for i in range(16)],
|
||||
_h(_DKD, 2.0, 100), _h(_DKT, 2.25, 65), _h(_DKT, 2.5, 68),
|
||||
_h(_DKD, 3.0, 110), _h(_DKD, 3.25, 105),
|
||||
_h(_DKD, 3.5, 115), _h(_DKT, 3.75, 80),
|
||||
],
|
||||
)
|
||||
|
||||
# Doumbek accent — syncopated dum-tek-ka pattern
|
||||
Pattern._FILLS["doumbek accent"] = dict(
|
||||
name="doumbek accent fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(_DKD, 0.0, 95), _h(_DKT, 0.25, 65), _h(_DKK, 0.5, 50),
|
||||
_h(_DKT, 0.75, 68), _h(_DKD, 1.0, 90),
|
||||
_h(_DKT, 1.5, 72), _h(_DKK, 1.75, 52), _h(_DKD, 2.0, 100),
|
||||
_h(_DKT, 2.25, 68), _h(_DKT, 2.5, 70), _h(_DKT, 2.75, 72),
|
||||
_h(_DKD, 3.0, 110), _h(_DKD, 3.5, 115),
|
||||
],
|
||||
)
|
||||
|
||||
# ── Tabla fills ──────────────────────────────────────────────────────────
|
||||
_TNA = DrumSound.TABLA_NA
|
||||
_TDH = DrumSound.TABLA_DHA
|
||||
_TTT = DrumSound.TABLA_TIT
|
||||
_TKE = DrumSound.TABLA_KE
|
||||
_TGB = DrumSound.TABLA_GE_BEND
|
||||
_TGE = DrumSound.TABLA_GE
|
||||
_TTI = DrumSound.TABLA_TIN
|
||||
_T3 = 1.0 / 12.0
|
||||
|
||||
# Tihai — the classic 3x pattern landing on sam
|
||||
Pattern._FILLS["tihai"] = dict(
|
||||
name="tihai fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(_TDH, 0.0, 105), _h(_TNA, 0.25, 72), _h(_TTT, 0.5, 48),
|
||||
_h(_TKE, 0.75, 52), _h(_TDH, 1.0, 100),
|
||||
_h(_TDH, 1.25, 110), _h(_TNA, 1.5, 78), _h(_TTT, 1.75, 52),
|
||||
_h(_TKE, 2.0, 55), _h(_TDH, 2.25, 105),
|
||||
_h(_TDH, 2.5, 118), _h(_TNA, 2.75, 82), _h(_TTT, 3.0, 58),
|
||||
_h(_TKE, 3.25, 60), _h(_TDH, 3.5, 127),
|
||||
],
|
||||
)
|
||||
|
||||
# Chakkardar — 32nd triplet cascade into slam
|
||||
Pattern._FILLS["chakkardar"] = dict(
|
||||
name="chakkardar fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
*[_h(_TTT, i * _T3, 32 + i * 3) for i in range(12)],
|
||||
_h(_TDH, 1.0, 115), _h(_TGB, 1.5, 108),
|
||||
*[_h(_TTT, 2.0 + i * _T3, 35 + i * 3) for i in range(12)],
|
||||
_h(_TDH, 3.0, 120), _h(_TDH, 3.25, 115),
|
||||
_h(_TGB, 3.5, 120), _h(_TDH, 3.75, 127),
|
||||
],
|
||||
)
|
||||
|
||||
# Tiri kita fill — rapid 16th note dayan burst
|
||||
Pattern._FILLS["tiri kita"] = dict(
|
||||
name="tiri kita fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(_TTT, 0.0, 50), _h(_TTT, 0.125, 38), _h(_TKE, 0.25, 48),
|
||||
_h(_TNA, 0.5, 72), _h(_TTT, 0.75, 42),
|
||||
_h(_TDH, 1.0, 95), _h(_TTT, 1.25, 38), _h(_TTT, 1.5, 42),
|
||||
_h(_TKE, 1.75, 48), _h(_TNA, 2.0, 75),
|
||||
_h(_TTT, 2.25, 40), _h(_TTT, 2.5, 45), _h(_TKE, 2.75, 50),
|
||||
_h(_TDH, 3.0, 100), _h(_TNA, 3.25, 70),
|
||||
_h(_TDH, 3.5, 110), _h(_TGB, 3.75, 105),
|
||||
],
|
||||
)
|
||||
|
||||
# Bayan showcase — deep bass bends
|
||||
Pattern._FILLS["bayan"] = dict(
|
||||
name="bayan fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(_TGB, 0.0, 100), _h(_TNA, 0.5, 65),
|
||||
_h(_TGE, 1.0, 85), _h(_TGB, 1.5, 105),
|
||||
_h(_TNA, 2.0, 70), _h(_TKE, 2.25, 48),
|
||||
_h(_TGB, 2.5, 110), _h(_TDH, 3.0, 115),
|
||||
_h(_TGB, 3.5, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# Call and response — dayan speaks, bayan answers
|
||||
Pattern._FILLS["tabla call"] = dict(
|
||||
name="tabla call fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(_TNA, 0.0, 105), _h(_TNA, 0.25, 55), _h(_TTT, 0.5, 38),
|
||||
_h(_TNA, 0.75, 100),
|
||||
_h(_TGE, 1.0, 95), _h(_TGE, 1.25, 48), _h(_TGB, 1.5, 90),
|
||||
_h(_TNA, 2.0, 108), _h(_TTT, 2.125, 30), _h(_TTT, 2.25, 35),
|
||||
_h(_TNA, 2.5, 100),
|
||||
_h(_TGB, 3.0, 112), _h(_TKE, 3.25, 48),
|
||||
_h(_TDH, 3.5, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# ── Cajón fills ──────────────────────────────────────────────────────────
|
||||
|
||||
# Cajón flam run — slaps accelerating into bass hit
|
||||
Pattern._FILLS["cajon flam"] = dict(
|
||||
name="cajon flam fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(CSL, 0.0, 95), _h(CT, 0.125, 45), _h(CSL, 0.25, 90),
|
||||
_h(CB, 0.5, 100), _h(CT, 0.75, 50),
|
||||
_h(CSL, 1.0, 88), _h(CT, 1.125, 42), _h(CSL, 1.25, 92),
|
||||
_h(CT, 1.5, 55), _h(CSL, 1.75, 85),
|
||||
_h(CB, 2.0, 105), _h(CSL, 2.25, 75), _h(CT, 2.5, 48),
|
||||
_h(CSL, 2.75, 80), _h(CT, 2.875, 40),
|
||||
_h(CB, 3.0, 110), _h(CSL, 3.25, 90), _h(CSL, 3.5, 95),
|
||||
_h(CB, 3.75, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# Cajón rumble — fast taps building to slap accents
|
||||
Pattern._FILLS["cajon rumble"] = dict(
|
||||
name="cajon rumble fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
*[_h(CT, i * 0.125, 35 + i * 3) for i in range(16)],
|
||||
_h(CSL, 2.0, 95), _h(CSL, 2.5, 100),
|
||||
_h(CB, 3.0, 108), _h(CSL, 3.25, 88),
|
||||
_h(CB, 3.5, 112), _h(CSL, 3.75, 95),
|
||||
],
|
||||
)
|
||||
|
||||
# Cajón breakdown — syncopated bass-slap groove
|
||||
Pattern._FILLS["cajon breakdown"] = dict(
|
||||
name="cajon breakdown fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
_h(CB, 0.0, 100), _h(CT, 0.25, 45), _h(CSL, 0.5, 85),
|
||||
_h(CB, 1.0, 95), _h(CSL, 1.25, 78), _h(CT, 1.5, 50),
|
||||
_h(CSL, 1.75, 82),
|
||||
_h(CB, 2.0, 105), _h(CT, 2.125, 40), _h(CT, 2.25, 42),
|
||||
_h(CSL, 2.5, 90), _h(CT, 2.75, 48),
|
||||
_h(CB, 3.0, 115), _h(CSL, 3.25, 95),
|
||||
_h(CB, 3.5, 110), _h(CSL, 3.75, 100),
|
||||
],
|
||||
)
|
||||
|
||||
# ── Metal fills (using metal kit) ────────────────────────────────────────
|
||||
|
||||
# Metal triplet — double kick triplets with snare accents
|
||||
Pattern._FILLS["metal triplet"] = dict(
|
||||
name="metal triplet fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Triplet kick pattern (12 kicks across 4 beats = triplet 8ths)
|
||||
*[_h(MK, i * (1/3), 95 + (i % 3 == 0) * 15) for i in range(12)],
|
||||
# Snare accents on downbeats
|
||||
_h(MS, 0.0, 110), _h(MS, 1.0, 105),
|
||||
_h(MS, 2.0, 110), _h(MS, 3.0, 115),
|
||||
# Hat on upbeats
|
||||
_h(MH, 0.5, 60), _h(MH, 1.5, 60),
|
||||
_h(MH, 2.5, 65), _h(MH, 3.5, 70),
|
||||
],
|
||||
)
|
||||
|
||||
# Metal blastbeat variant — alternating snare/kick 32nds
|
||||
Pattern._FILLS["metal blast"] = dict(
|
||||
name="metal blast fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Alternating kick-snare at 32nd note speed for 2 beats
|
||||
*[_h(MK if i % 2 == 0 else MS, i * 0.125, 100 + i) for i in range(16)],
|
||||
# Then crash into half-time for 2 beats
|
||||
_h(MK, 2.0, 120), _h(MS, 2.5, 115),
|
||||
_h(MK, 3.0, 120), _h(MH, 3.25, 80),
|
||||
_h(MS, 3.5, 120),
|
||||
],
|
||||
)
|
||||
|
||||
# Metal cascade — descending snare/kick rolls
|
||||
Pattern._FILLS["metal cascade"] = dict(
|
||||
name="metal cascade fill",
|
||||
time_signature="4/4",
|
||||
beats=4.0,
|
||||
hits=[
|
||||
# Fast snare roll beat 1
|
||||
*[_h(MS, i * 0.125, 80 + i * 5) for i in range(8)],
|
||||
# Double kick beat 2
|
||||
*[_h(MK, 1.0 + i * 0.125, 90 + i * 3) for i in range(8)],
|
||||
# Alternating beat 3
|
||||
_h(MS, 2.0, 105), _h(MK, 2.125, 95),
|
||||
_h(MS, 2.25, 108), _h(MK, 2.375, 98),
|
||||
_h(MS, 2.5, 110), _h(MK, 2.625, 100),
|
||||
_h(MS, 2.75, 112), _h(MK, 2.875, 102),
|
||||
# Crash ending
|
||||
_h(MK, 3.0, 120), _h(MS, 3.0, 120),
|
||||
_h(MK, 3.5, 120), _h(MS, 3.5, 120),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class Part:
|
||||
"""A named voice within a Score, with its own synth, envelope, and effects.
|
||||
@@ -2223,6 +2526,35 @@ class Part:
|
||||
bend_type=bend_type, lyric=lyric))
|
||||
return self
|
||||
|
||||
def hold(self, tone_or_string, duration=Duration.QUARTER, *, velocity: int = 100,
|
||||
bend: float = 0.0, bend_type: str = "smooth", lyric: str = "") -> "Part":
|
||||
"""Add a note without advancing the beat position.
|
||||
|
||||
The note plays at the current position but the next note
|
||||
starts at the *same* time — enabling polyphonic overlap
|
||||
on a single part.
|
||||
|
||||
Use this for: piano sustain pedal (bass note rings while
|
||||
melody plays above), guitar strumming with individual
|
||||
string timing, held drone notes under a melody.
|
||||
|
||||
Example::
|
||||
|
||||
>>> piano = score.part("piano", instrument="piano")
|
||||
>>> piano.hold("C3", Duration.WHOLE) # bass rings for 4 beats
|
||||
>>> piano.add("E4", Duration.HALF) # starts at same time as C3
|
||||
>>> piano.add("G4", Duration.HALF) # starts at beat 2
|
||||
"""
|
||||
if isinstance(tone_or_string, str):
|
||||
from .tones import Tone
|
||||
tone_or_string = Tone.from_string(tone_or_string, system=self._system)
|
||||
if isinstance(duration, (int, float)):
|
||||
duration = _RawDuration(duration)
|
||||
self.notes.append(Note(tone=tone_or_string, duration=duration,
|
||||
velocity=velocity, bend=bend,
|
||||
bend_type=bend_type, lyric=lyric, _hold=True))
|
||||
return self
|
||||
|
||||
def set(self, **params) -> "Part":
|
||||
"""Change effect parameters at the current beat position.
|
||||
|
||||
@@ -2573,21 +2905,14 @@ class Part:
|
||||
from .chords import Chord as ChordClass
|
||||
chord_obj = ChordClass(tones=strum_tones)
|
||||
|
||||
# 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.
|
||||
# Strum: hold a quiet leading string simultaneously with the
|
||||
# full chord using hold(). No timing gap — both start at the
|
||||
# same beat position. The leading string adds strum texture.
|
||||
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)
|
||||
if strum_time > 0 and n_strings >= 3:
|
||||
grace_vel = max(1, int(velocity * 0.15))
|
||||
self.hold(strum_tones[0], total_beats, velocity=grace_vel)
|
||||
self.add(chord_obj, total_beats, velocity=velocity)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
+15
-2
@@ -4869,6 +4869,19 @@ def test_duration_values():
|
||||
assert abs(Duration.TRIPLET_QUARTER.value - 2 / 3) < 1e-9
|
||||
|
||||
|
||||
def test_duration_arithmetic():
|
||||
# Multiplication
|
||||
assert Duration.WHOLE * 2 == 8.0
|
||||
assert 2 * Duration.HALF == 4.0
|
||||
assert Duration.QUARTER * 3 == 3.0
|
||||
# Division
|
||||
assert Duration.WHOLE / 2 == 2.0
|
||||
# Addition
|
||||
assert Duration.HALF + Duration.QUARTER == 3.0
|
||||
assert Duration.HALF + 1.0 == 3.0
|
||||
assert 1.0 + Duration.HALF == 3.0
|
||||
|
||||
|
||||
def test_time_signature_from_string_4_4():
|
||||
ts = TimeSignature.from_string("4/4")
|
||||
assert ts.beats == 4
|
||||
@@ -5320,7 +5333,7 @@ def test_supersaw_wave():
|
||||
@needs_portaudio
|
||||
def test_all_synths_in_enum():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 41
|
||||
assert len(Synth) == 42
|
||||
for s in Synth:
|
||||
wave = s(440, n_samples=1000)
|
||||
assert len(wave) == 1000
|
||||
@@ -7142,7 +7155,7 @@ def test_score_system_propagates():
|
||||
|
||||
def test_synth_enum_count():
|
||||
from pytheory.play import Synth
|
||||
assert len(Synth) == 41
|
||||
assert len(Synth) == 42
|
||||
|
||||
|
||||
def test_all_synths_render_and_enum_match():
|
||||
|
||||
Reference in New Issue
Block a user