mirror of
https://github.com/kennethreitz/pytheory.git
synced 2026-06-05 23:00:20 +00:00
119dd2d921
Sequencing: Score concept, time signatures for non-musicians, Parts as DAW tracks, arpeggiator/legato/glide context with TB-303 and acid history. Synths: synthesis philosophy, DX7/Juno/JP-8000 history, practical combos. Effects: real-music context (The Edge, DJ knobs, shower reverb), why signal chain order matters, automation as breathing, LFO as repeating automation. Drums: drums-as-genre foundation, genre group cultural context, fills as transition signals, drum synthesis as real drum machine techniques. Playback: three output options context, MIDI as the working musician's path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
343 lines
11 KiB
ReStructuredText
343 lines
11 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:: pycon
|
|
|
|
>>> from pytheory import Score, Duration, Tone
|
|
|
|
>>> score = Score("4/4", bpm=120)
|
|
>>> score.add(Tone.from_string("C4", system="western"), Duration.QUARTER)
|
|
<Score 4/4 120bpm ...>
|
|
>>> score.add(Tone.from_string("E4", system="western"), Duration.QUARTER)
|
|
<Score 4/4 120bpm ...>
|
|
>>> score.add(Tone.from_string("G4", system="western"), Duration.HALF)
|
|
<Score 4/4 120bpm ...>
|
|
|
|
>>> score.total_beats
|
|
4.0
|
|
>>> score.measures
|
|
1.0
|
|
>>> score.duration_ms
|
|
2000.0
|
|
|
|
Rests
|
|
~~~~~
|
|
|
|
Add silence with ``.rest()``:
|
|
|
|
.. code-block:: pycon
|
|
|
|
>>> score = Score("4/4", bpm=120)
|
|
>>> score.add(Tone.from_string("C4", system="western"), Duration.HALF)
|
|
<Score 4/4 120bpm ...>
|
|
>>> score.rest(Duration.HALF)
|
|
<Score 4/4 120bpm ...>
|
|
>>> score.measures
|
|
1.0
|
|
|
|
Chords
|
|
~~~~~~
|
|
|
|
Chords work just like tones — pass any ``Chord`` object:
|
|
|
|
.. code-block:: pycon
|
|
|
|
>>> 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)
|
|
|
|
>>> 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:: pycon
|
|
|
|
>>> 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)
|
|
|
|
>>> 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:: pycon
|
|
|
|
>>> 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:: pycon
|
|
|
|
>>> lead.add("E5", Duration.QUARTER).add("D5", Duration.EIGHTH).rest(Duration.EIGHTH)
|
|
<Part 'lead' ...>
|
|
|
|
Raw float beats work too — useful for swing and tuplets:
|
|
|
|
.. code-block:: pycon
|
|
|
|
>>> lead.add("C5", 0.67).add("B4", 0.33).add("A4", 1.0)
|
|
<Part 'lead' ...>
|
|
|
|
Chords and Tone objects work the same way:
|
|
|
|
.. code-block:: pycon
|
|
|
|
>>> 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:: pycon
|
|
|
|
>>> 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:: pycon
|
|
|
|
>>> 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:: pycon
|
|
|
|
>>> 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:: pycon
|
|
|
|
>>> 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",.67),("D5",.33),("C5",.67),("B4",.33),
|
|
... ("A4",1),("C5",.67),("E5",.33),("D5",.67),("C5",.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)
|