mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
2fc5aae678
Effects: sidechain pump with parameters, practical examples, tip about not sidechaining everything. Sequencing: song sections with verse/chorus/repeat workflow, custom names, real songwriting analogy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
577 lines
16 KiB
ReStructuredText
577 lines
16 KiB
ReStructuredText
Sequencing
|
||
==========
|
||
|
||
The sequencing system lets you compose multi-part arrangements with
|
||
durations, time signatures, and instrument voices. This is where
|
||
PyTheory goes from theory tool to composition tool.
|
||
|
||
At the center of everything is the ``Score``. Think of it as your
|
||
arrangement, your song, your sketch pad. It holds the tempo, the time
|
||
signature, the drum pattern, and every instrument part you create. If
|
||
you've ever used a DAW, the Score is your session file. If you haven't,
|
||
it's the sheet of paper where the whole piece lives. Everything you
|
||
compose -- melodies, chord progressions, bass lines, arpeggios -- gets
|
||
added to a Score before you can hear it, export it, or do anything
|
||
useful with it.
|
||
|
||
Duration
|
||
--------
|
||
|
||
In music, all rhythm boils down to one convention: the quarter note
|
||
equals one beat. Everything else is relative to that. A whole note is
|
||
four beats. An eighth note is half a beat. This is how musicians have
|
||
communicated timing for centuries, and it's how PyTheory works too.
|
||
Once you internalize "quarter note = 1 beat," durations become
|
||
intuitive arithmetic.
|
||
|
||
A ``Duration`` represents a note length in beats (quarter note = 1 beat):
|
||
|
||
.. code-block:: pycon
|
||
|
||
>>> from pytheory import Duration
|
||
|
||
>>> Duration.WHOLE.value
|
||
4.0
|
||
>>> Duration.HALF.value
|
||
2.0
|
||
>>> Duration.QUARTER.value
|
||
1.0
|
||
>>> Duration.EIGHTH.value
|
||
0.5
|
||
>>> Duration.SIXTEENTH.value
|
||
0.25
|
||
>>> Duration.DOTTED_HALF.value
|
||
3.0
|
||
>>> Duration.DOTTED_QUARTER.value
|
||
1.5
|
||
>>> Duration.TRIPLET_QUARTER.value
|
||
0.6666666666666666
|
||
|
||
Time Signatures
|
||
---------------
|
||
|
||
If you're not a musician, time signatures can seem mysterious. They're
|
||
not. The top number tells you how many beats are in a bar. The bottom
|
||
number tells you which note value gets one beat. That's it.
|
||
|
||
In practice, you only need to know a handful:
|
||
|
||
- **4/4** -- four beats per bar. This is the default. Almost all pop,
|
||
rock, hip hop, electronic, and R&B music is in 4/4. If you're not
|
||
sure, use this.
|
||
- **3/4** -- three beats per bar. The waltz feel. Think "Blue Danube"
|
||
or Radiohead's "Everything in Its Right Place."
|
||
- **6/8** -- six eighth notes per bar, grouped in two sets of three.
|
||
Each group feels like one big swaying beat. Folk music, slow jams,
|
||
ballads.
|
||
- **12/8** -- twelve eighth notes per bar, grouped in four sets of
|
||
three. The slow blues shuffle, the gospel feel, "At Last" by Etta
|
||
James. Each "big beat" has a triplet swing baked into it.
|
||
|
||
A ``TimeSignature`` holds the meter of a piece -- how many beats per
|
||
measure and which note value gets one beat:
|
||
|
||
.. code-block:: pycon
|
||
|
||
>>> from pytheory.rhythm import TimeSignature
|
||
|
||
>>> ts = TimeSignature.from_string("4/4")
|
||
>>> ts.beats_per_measure
|
||
4.0
|
||
|
||
>>> TimeSignature.from_string("3/4").beats_per_measure
|
||
3.0
|
||
|
||
>>> TimeSignature.from_string("6/8").beats_per_measure
|
||
3.0
|
||
|
||
>>> TimeSignature.from_string("12/8").beats_per_measure
|
||
6.0
|
||
|
||
The ``beats_per_measure`` is always in quarter-note units. In 6/8,
|
||
there are 6 eighth notes per bar = 3 quarter-note beats. In 12/8,
|
||
12 eighth notes = 6 quarter-note beats, grouped in four dotted-quarter
|
||
pulses.
|
||
|
||
Score Basics
|
||
------------
|
||
|
||
A ``Score`` is a sequence of notes and rests with a time signature and
|
||
tempo. Use ``.add()`` and ``.rest()`` for fluent chaining:
|
||
|
||
.. code-block:: python
|
||
|
||
from pytheory import Score, Duration, Tone
|
||
|
||
score = Score("4/4", bpm=120)
|
||
score.add(Tone.from_string("C4", system="western"), Duration.QUARTER)
|
||
score.add(Tone.from_string("E4", system="western"), Duration.QUARTER)
|
||
score.add(Tone.from_string("G4", system="western"), Duration.HALF)
|
||
|
||
.. code-block:: pycon
|
||
|
||
>>> score.total_beats
|
||
4.0
|
||
>>> score.measures
|
||
1.0
|
||
>>> score.duration_ms
|
||
2000.0
|
||
|
||
Rests
|
||
~~~~~
|
||
|
||
Add silence with ``.rest()``:
|
||
|
||
.. code-block:: python
|
||
|
||
score = Score("4/4", bpm=120)
|
||
score.add(Tone.from_string("C4", system="western"), Duration.HALF)
|
||
score.rest(Duration.HALF)
|
||
|
||
.. code-block:: pycon
|
||
|
||
>>> score.measures
|
||
1.0
|
||
|
||
Chords
|
||
~~~~~~
|
||
|
||
Chords work just like tones — pass any ``Chord`` object:
|
||
|
||
.. code-block:: python
|
||
|
||
from pytheory import Score, Duration, Key
|
||
|
||
key = Key("C", "major")
|
||
chords = key.progression("I", "V", "vi", "IV")
|
||
|
||
score = Score("4/4", bpm=120)
|
||
for chord in chords:
|
||
score.add(chord, Duration.WHOLE)
|
||
|
||
.. code-block:: pycon
|
||
|
||
>>> score.measures
|
||
4.0
|
||
>>> score.duration_ms
|
||
8000.0
|
||
|
||
Compound Time
|
||
~~~~~~~~~~~~~
|
||
|
||
12/8 is a compound meter — 12 eighth notes per bar grouped in four
|
||
groups of three. Each group feels like one "big beat":
|
||
|
||
.. code-block:: python
|
||
|
||
from pytheory import Score, Duration, Key
|
||
|
||
key = Key("A", "minor")
|
||
chords = key.random_progression(4)
|
||
|
||
score = Score("12/8", bpm=120)
|
||
for c in chords:
|
||
score.add(c, Duration.DOTTED_HALF)
|
||
score.add(c, Duration.DOTTED_HALF)
|
||
|
||
.. code-block:: pycon
|
||
|
||
>>> score.measures
|
||
4.0
|
||
|
||
Parts
|
||
-----
|
||
|
||
Parts are like tracks in a DAW. Each one has its own instrument sound
|
||
(synth waveform + envelope), its own volume level, and its own effects
|
||
chain. When you call ``play_score()``, all the parts get mixed together
|
||
into a single audio stream -- just like hitting play in Logic or
|
||
Ableton. You might have a pad part holding down chords, a lead part
|
||
playing a melody, and a bass part holding down the low end. Each one
|
||
is independent: different synth, different envelope, different effects.
|
||
|
||
The ``Part`` class lets you layer multiple instrument voices -- each with
|
||
its own synth waveform, ADSR envelope, and volume level. Create parts
|
||
with ``Score.part()``:
|
||
|
||
.. code-block:: python
|
||
|
||
from pytheory import Score, Key, Duration, Chord
|
||
from pytheory.play import play_score
|
||
|
||
score = Score("4/4", bpm=140)
|
||
|
||
chords = score.part("chords", synth="sine", envelope="pad", volume=0.35)
|
||
lead = score.part("lead", synth="saw", envelope="pluck", volume=0.5)
|
||
bass = score.part("bass", synth="triangle", envelope="pluck", volume=0.45)
|
||
|
||
Adding Notes to Parts
|
||
~~~~~~~~~~~~~~~~~~~~~
|
||
|
||
Parts accept note strings directly — no need to wrap in
|
||
``Tone.from_string()``. ``.add()`` and ``.rest()`` return self for
|
||
fluent chaining:
|
||
|
||
.. code-block:: python
|
||
|
||
lead.add("E5", Duration.QUARTER).add("D5", Duration.EIGHTH).rest(Duration.EIGHTH)
|
||
|
||
Raw float beats work too — useful for swing and tuplets:
|
||
|
||
.. code-block:: python
|
||
|
||
lead.add("C5", 0.67).add("B4", 0.33).add("A4", 1.0)
|
||
|
||
Chords and Tone objects work the same way:
|
||
|
||
.. code-block:: python
|
||
|
||
for chord in Key("A", "minor").progression("i", "iv", "V", "i"):
|
||
chords.add(chord, Duration.WHOLE)
|
||
|
||
for note in ["A2", "C3", "E3", "A2", "D2", "F2", "A2", "D2"]:
|
||
bass.add(note, Duration.QUARTER)
|
||
|
||
Arpeggiator
|
||
------------
|
||
|
||
An arpeggiator takes a chord and plays its notes one at a time, in a
|
||
pattern, automatically. You hold down a chord and it ripples through
|
||
the notes -- up, down, up-and-down, random. It's one of the most
|
||
iconic sounds in electronic music. The bubbly bass lines of acid house,
|
||
the cascading runs of 80s synth pop (think "Jump" or "Take On Me"),
|
||
the hypnotic patterns of trance -- all arpeggiators. It turns a simple
|
||
three-note chord into a rhythmic, melodic engine.
|
||
|
||
``Part.arpeggio()`` takes a chord and sequences through its notes
|
||
automatically -- like a hardware arpeggiator on a synth:
|
||
|
||
.. code-block:: python
|
||
|
||
lead = score.part(
|
||
"lead",
|
||
synth="saw",
|
||
legato=True,
|
||
glide=0.03,
|
||
distortion=0.8,
|
||
lowpass=1000,
|
||
lowpass_q=5.0,
|
||
)
|
||
lead.arpeggio(
|
||
Chord.from_symbol("Cm"),
|
||
bars=2,
|
||
pattern="up",
|
||
division=Duration.SIXTEENTH,
|
||
octaves=2,
|
||
)
|
||
|
||
Parameters:
|
||
|
||
- ``chord``: A Chord object or string like ``"Am"``.
|
||
- ``bars``: Number of bars to fill (default 1).
|
||
- ``pattern``: ``"up"``, ``"down"``, ``"updown"``, ``"downup"``, ``"random"``.
|
||
- ``division``: Step length (default ``Duration.SIXTEENTH``).
|
||
- ``octaves``: Octave span (default 1). With 2, the pattern repeats one octave up.
|
||
|
||
Chain arpeggios through a progression:
|
||
|
||
.. code-block:: python
|
||
|
||
for sym in ["Cm", "Fm", "Abm", "Gm"]:
|
||
lead.arpeggio(sym, bars=2, pattern="updown", octaves=2)
|
||
|
||
Combined with legato, glide, distortion, and a resonant lowpass, this
|
||
produces the classic acid/trance arpeggiator sound.
|
||
|
||
Legato and Glide
|
||
----------------
|
||
|
||
Normally, every note you play has its own life cycle -- the sound
|
||
attacks, sustains, and releases before the next note begins. You hear
|
||
each note as a separate event. Legato changes that. The Italian word
|
||
means "tied together," and that's exactly what it does: the envelope
|
||
flows continuously from one note to the next with no retriggering. The
|
||
pitch changes, but the sound never dies and restarts.
|
||
|
||
Glide (also called portamento) takes this further. Instead of the pitch
|
||
jumping instantly from one note to the next, it *slides* -- a smooth,
|
||
continuous pitch sweep. This is THE sound of the Roland TB-303, the
|
||
little silver box that accidentally invented acid house. A saw wave
|
||
with legato, glide, a resonant lowpass filter, and some distortion --
|
||
that's the entire genre right there.
|
||
|
||
By default, each note gets its own attack/release envelope. ``legato=True``
|
||
renders the entire part as one continuous waveform -- the pitch changes
|
||
at note boundaries but the envelope flows unbroken. Add ``glide`` for
|
||
portamento (pitch slides between notes):
|
||
|
||
.. code-block:: python
|
||
|
||
acid = score.part(
|
||
"acid",
|
||
synth="saw",
|
||
envelope="pad",
|
||
legato=True,
|
||
glide=0.04,
|
||
)
|
||
acid.add("C2", 0.25).add("C3", 0.25).add("G2", 0.25).add("C2", 0.25)
|
||
|
||
- ``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.
|
||
|
||
Complete Example
|
||
----------------
|
||
|
||
A full multi-part arrangement built from scratch — bossa nova with FM
|
||
rhodes, triangle lead, and filtered bass:
|
||
|
||
.. code-block:: python
|
||
|
||
from pytheory import Score, Pattern, Key, Duration, Chord
|
||
from pytheory.play import play_score
|
||
|
||
score = Score("4/4", bpm=140)
|
||
score.drums("bossa nova", repeats=4)
|
||
|
||
# FM rhodes with reverb
|
||
rhodes = score.part(
|
||
"rhodes",
|
||
synth="fm",
|
||
envelope="piano",
|
||
volume=0.3,
|
||
reverb=0.4,
|
||
reverb_decay=1.8,
|
||
)
|
||
|
||
# Triangle 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,
|
||
)
|
||
|
||
# Filtered bass
|
||
bass = score.part(
|
||
"bass",
|
||
synth="sine",
|
||
envelope="pluck",
|
||
volume=0.45,
|
||
lowpass=600,
|
||
)
|
||
|
||
for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
|
||
rhodes.add(Chord.from_symbol(sym), 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)
|
||
|
||
for n in ["A2", "E2", "A2", "C3", "D2", "A2", "D2", "F2"]:
|
||
bass.add(n, Duration.QUARTER)
|
||
|
||
play_score(score)
|
||
|
||
Velocity
|
||
--------
|
||
|
||
Real music has dynamics — accents are louder, ghost notes are barely
|
||
there, phrases crescendo and decrescendo. Every note can have its own
|
||
velocity (1–127, where 100 is the default):
|
||
|
||
.. code-block:: python
|
||
|
||
lead.add("C5", Duration.QUARTER, velocity=120) # loud accent
|
||
lead.add("D5", Duration.QUARTER, velocity=40) # ghost note
|
||
lead.add("E5", Duration.QUARTER) # default (100)
|
||
|
||
The arpeggiator also accepts velocity:
|
||
|
||
.. code-block:: python
|
||
|
||
lead.arpeggio("Am", bars=2, pattern="up", velocity=80)
|
||
|
||
Swing and Groove
|
||
----------------
|
||
|
||
Perfectly quantized music sounds robotic. Swing delays every other
|
||
subdivision by a percentage, giving the rhythm a human, shuffled feel.
|
||
Jazz swings hard. Bossa nova swings gently. Hip hop has its own pocket.
|
||
|
||
Set swing on the Score (applies to everything) or per-Part:
|
||
|
||
.. code-block:: python
|
||
|
||
# Triplet swing — lazy jazz feel
|
||
score = Score("4/4", bpm=100, swing=0.55)
|
||
|
||
# Per-part override — the lead swings harder than the bass
|
||
lead = score.part("lead", synth="saw", swing=0.6)
|
||
bass = score.part("bass", synth="sine", swing=0.4)
|
||
|
||
Swing values:
|
||
|
||
- **0.0** = perfectly straight (default)
|
||
- **0.3** = subtle shuffle (pop, R&B)
|
||
- **0.5** = triplet feel (jazz, blues)
|
||
- **0.67** = hard swing (bebop)
|
||
|
||
Tempo Changes
|
||
-------------
|
||
|
||
Real music doesn't stay at one tempo. Songs speed up for energy,
|
||
slow down for endings, and sometimes shift abruptly. Use
|
||
``score.set_tempo()`` to change BPM at the current position:
|
||
|
||
.. code-block:: python
|
||
|
||
score = Score("4/4", bpm=90)
|
||
|
||
# Verse: slow and moody
|
||
lead.add("D5", Duration.WHOLE)
|
||
lead.add("F5", Duration.WHOLE)
|
||
|
||
# Chorus: speeds up
|
||
score.set_tempo(110)
|
||
lead.add("A5", Duration.WHOLE)
|
||
lead.add("D6", Duration.WHOLE)
|
||
|
||
# Outro: slows way down
|
||
score.set_tempo(70)
|
||
lead.add("D5", Duration.WHOLE)
|
||
|
||
The tempo map engine handles the math — beat positions are converted
|
||
to sample positions accounting for every tempo change.
|
||
|
||
Fades
|
||
-----
|
||
|
||
``Part.fade_in()`` and ``Part.fade_out()`` ramp the volume over a
|
||
number of bars. They work by generating automation points, so they
|
||
integrate naturally with the rest of the automation system:
|
||
|
||
.. code-block:: python
|
||
|
||
pad = score.part(
|
||
"pad",
|
||
synth="supersaw",
|
||
envelope="pad",
|
||
volume=0.3,
|
||
reverb=0.5,
|
||
)
|
||
|
||
# Fade in over first 4 bars
|
||
pad.fade_in(bars=4)
|
||
for chord in chords:
|
||
pad.add(chord, Duration.WHOLE)
|
||
|
||
# Fade out over last 2 bars
|
||
pad.fade_out(bars=2)
|
||
pad.rest(Duration.WHOLE)
|
||
pad.rest(Duration.WHOLE)
|
||
|
||
Humanize
|
||
--------
|
||
|
||
Perfectly quantized music sounds like a machine made it — because it
|
||
did. Real musicians are never exactly on the beat. Their timing drifts
|
||
by a few milliseconds, their velocity varies from note to note. These
|
||
imperfections are what make music feel *alive*.
|
||
|
||
The ``humanize`` parameter adds random micro-variations in both timing
|
||
and velocity at render time. The score data stays clean and
|
||
deterministic — the randomness is only applied during playback.
|
||
|
||
.. code-block:: python
|
||
|
||
# Subtle — like a very tight session player
|
||
lead = score.part("lead", synth="saw", humanize=0.1)
|
||
|
||
# Natural — like a good live take
|
||
rhodes = score.part("rhodes", synth="fm", humanize=0.3)
|
||
|
||
# Loose — like a late-night jam after a few drinks
|
||
bass = score.part("bass", synth="sine", humanize=0.5)
|
||
|
||
Humanize values:
|
||
|
||
- **0.0** = perfectly quantized (default)
|
||
- **0.1** = subtle, studio-tight
|
||
- **0.2–0.3** = natural, like a real player
|
||
- **0.4–0.5** = loose, relaxed, human
|
||
- **0.6+** = sloppy (sometimes that's what you want)
|
||
|
||
Combine with swing for the most realistic feel:
|
||
|
||
.. code-block:: python
|
||
|
||
score = Score("4/4", bpm=95, swing=0.45)
|
||
lead = score.part(
|
||
"lead",
|
||
synth="saw",
|
||
envelope="pluck",
|
||
humanize=0.3,
|
||
delay=0.2,
|
||
reverb=0.25,
|
||
)
|
||
|
||
Song Structure
|
||
--------------
|
||
|
||
Real songs aren't one long stream of notes — they have verses,
|
||
choruses, bridges, drops. The section system lets you name blocks
|
||
of your arrangement, then repeat them without rewriting everything.
|
||
|
||
This is how actual songwriting works: you write a verse, you write
|
||
a chorus, then you arrange them — verse, verse, chorus, verse,
|
||
chorus, chorus, outro. The sections are the building blocks;
|
||
the arrangement is the order you play them in.
|
||
|
||
Define sections with ``score.section()`` and repeat them with
|
||
``score.repeat()``:
|
||
|
||
.. code-block:: python
|
||
|
||
score = Score("4/4", bpm=124)
|
||
score.drums("house", repeats=16)
|
||
|
||
pad = score.part("pad", synth="supersaw", envelope="pad")
|
||
lead = score.part("lead", synth="saw", envelope="pluck")
|
||
bass = score.part("bass", synth="sine", lowpass=300)
|
||
|
||
# ── Define the verse ──
|
||
score.section("verse")
|
||
for sym in ["Cm", "Ab", "Eb", "Bb"]:
|
||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||
lead.add("C5", 1).add("Eb5", 1).rest(2)
|
||
for n in ["C1", "C1", "Ab0", "Ab0", "Eb1", "Eb1", "Bb0", "Bb0"]:
|
||
bass.add(n, Duration.HALF)
|
||
|
||
# ── Define the chorus ──
|
||
score.section("chorus")
|
||
lead.set(lowpass=5000, reverb=0.3)
|
||
for sym in ["Cm", "Fm", "Ab", "Gm"]:
|
||
pad.add(Chord.from_symbol(sym), Duration.WHOLE)
|
||
lead.add("C6", 1).add("Bb5", 1).add("G5", 1).rest(1)
|
||
for n in ["C1", "C1", "F1", "F1", "Ab0", "Ab0", "G1", "G1"]:
|
||
bass.add(n, Duration.HALF)
|
||
score.end_section()
|
||
|
||
# ── Arrange: verse, chorus, verse, chorus, chorus ──
|
||
score.repeat("verse")
|
||
score.repeat("chorus")
|
||
score.repeat("verse")
|
||
score.repeat("chorus", times=2)
|
||
|
||
Use any names you want — ``"intro"``, ``"verse"``, ``"chorus"``,
|
||
``"bridge"``, ``"drop"``, ``"breakdown"``, ``"outro"``, or anything
|
||
that makes sense for your song. The names are just labels.
|