Update rhythm and playback docs with Part class, multi-part examples

- Rhythm guide: full Part API docs with synths, envelopes, chaining,
  raw float beats, headless rendering, complete bossa nova example
- Playback guide: rewrite intro with quick-start showing both simple
  and expressive usage, update all Score examples to use named parts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 11:30:09 -04:00
parent d0e8e43b56
commit 83a988d085
2 changed files with 204 additions and 50 deletions
+71 -22
View File
@@ -1,8 +1,10 @@
Audio Playback
==============
PyTheory can synthesize and play tones and chords through your speakers
using basic `waveform <https://en.wikipedia.org/wiki/Waveform>`_ synthesis.
PyTheory includes a complete audio engine — synthesize tones, chords,
drum patterns, and multi-part arrangements, then play them through your
speakers or save to WAV/MIDI. No samples or external audio files needed;
everything is generated from waveforms and envelopes.
.. note::
@@ -10,6 +12,36 @@ using basic `waveform <https://en.wikipedia.org/wiki/Waveform>`_ synthesis.
installed on your system. On macOS: ``brew install portaudio``.
On Ubuntu: ``apt install libportaudio2``.
Quick Start
-----------
The simplest thing you can do:
.. code-block:: pycon
>>> from pytheory import Tone, Chord, play
>>> play(Tone.from_string("A4"), t=1_000) # A440 for 1 second
>>> play(Chord.from_symbol("Am7"), t=2_000) # chord for 2 seconds
The most expressive thing you can do:
.. code-block:: pycon
>>> from pytheory import Score, Pattern, Key, Duration, Chord
>>> from pytheory.play import play_score
>>> score = Score("4/4", bpm=140)
>>> score.add_pattern(Pattern.preset("bossa nova"), repeats=4)
>>> chords = score.part("chords", synth="sine", envelope="pad")
>>> lead = score.part("lead", synth="saw", envelope="pluck")
>>> bass = score.part("bass", synth="triangle", envelope="pluck")
>>> for sym in ["Am", "Dm", "E7", "Am"]:
... chords.add(Chord.from_symbol(sym), Duration.WHOLE)
>>> lead.add("E5", 0.67).add("D5", 0.33).add("C5", 1.0)
>>> bass.add("A2", Duration.HALF).add("E2", Duration.HALF)
>>> play_score(score)
Everything between those two extremes is documented below.
Playing a Tone
--------------
@@ -158,38 +190,55 @@ Envelopes work with all functions — ``play()``, ``save()``, and
Playing a Score
---------------
A ``Score`` combines drum patterns and chord progressions into a single
playable arrangement. ``play_score()`` renders everything — synthesized
drums and tonal chords — mixed together in one audio buffer:
A ``Score`` combines drum patterns, chord pads, melody leads, bass lines,
and any number of named parts — each with its own synth voice — into a
single playable arrangement.
.. code-block:: pycon
>>> from pytheory import Pattern, Key, Duration
>>> from pytheory import Score, Pattern, Key, Duration, Chord
>>> from pytheory.play import play_score
>>> key = Key("A", "minor")
>>> score = Pattern.preset("bossa nova").to_score(repeats=4, bpm=140)
>>> for chord in key.progression("i", "iv", "V", "i"):
... score.add(chord, Duration.WHOLE)
... score.add(chord, Duration.WHOLE)
>>> score = Score("4/4", bpm=140)
>>> score.add_pattern(Pattern.preset("bossa nova"), repeats=4)
>>> chords = score.part("chords", synth="sine", envelope="pad", volume=0.3)
>>> lead = score.part("lead", synth="triangle", envelope="pluck", volume=0.5)
>>> bass = score.part("bass", synth="sine", envelope="pluck", volume=0.45)
>>> for sym in ["Am", "Dm", "E7", "Am"]:
... chords.add(Chord.from_symbol(sym), Duration.WHOLE)
... chords.add(Chord.from_symbol(sym), Duration.WHOLE)
>>> lead.add("E5", 0.67).add("D5", 0.33).add("C5", 1.0).rest(0.5)
>>> for n in ["A2", "E2", "A2", "C3", "D2", "A2", "D2", "F2"]:
... bass.add(n, Duration.QUARTER)
>>> play_score(score)
Try different combinations:
More examples:
.. code-block:: pycon
>>> # Salsa with a ii-V-I
>>> key = Key("C", "major")
>>> score = Pattern.preset("salsa").to_score(repeats=4, bpm=180)
>>> for chord in key.progression("ii", "V", "I", "I") * 2:
... score.add(chord, Duration.WHOLE)
>>> # Salsa with a ii-V-I — saw lead over clave
>>> score = Score("4/4", bpm=180)
>>> score.add_pattern(Pattern.preset("salsa"), repeats=4)
>>> chords = score.part("chords", synth="sine", envelope="pad")
>>> lead = score.part("lead", synth="saw", envelope="pluck", volume=0.4)
>>> for chord in Key("C", "major").progression("ii", "V", "I", "I") * 2:
... chords.add(chord, Duration.WHOLE)
>>> lead.add("D5", 0.67).add("F5", 0.33).add("A5", 1.0)
>>> play_score(score)
>>> # Jazz ballad
>>> key = Key("Bb", "major")
>>> score = Pattern.preset("jazz").to_score(repeats=8, bpm=90)
>>> for chord in key.progression("I", "vi", "ii", "V") * 2:
... score.add(chord, Duration.WHOLE)
>>> # Jazz ballad — triangle lead, walking bass
>>> score = Score("4/4", bpm=90)
>>> score.add_pattern(Pattern.preset("jazz"), repeats=8)
>>> pads = score.part("pads", synth="sine", envelope="pad", volume=0.3)
>>> lead = score.part("lead", synth="triangle", envelope="pluck", volume=0.5)
>>> bass = score.part("bass", synth="sine", envelope="pluck", volume=0.4)
>>> for chord in Key("Bb", "major").progression("I", "vi", "ii", "V") * 2:
... pads.add(chord, Duration.WHOLE)
>>> play_score(score)
MIDI Export
+133 -28
View File
@@ -226,24 +226,118 @@ Convert any pattern to a Score, then export:
>>> Pattern.preset("salsa").to_score(repeats=4, bpm=180).save_midi("salsa.mid")
>>> Pattern.preset("afrobeat").to_score(repeats=8, bpm=110).save_midi("afrobeat.mid")
Combining Drums with Chords
----------------------------
Multi-Part Arrangements
-----------------------
You can layer a drum pattern with a chord progression by adding chord
notes to the same Score:
The ``Part`` class lets you layer multiple instrument voices — each with
its own synth waveform, ADSR envelope, and volume level. This is where
PyTheory goes from "theory tool" to "composition tool."
Create parts with ``Score.part()``:
.. code-block:: pycon
>>> from pytheory import Pattern, Key, Duration
>>> from pytheory import Score, Pattern, Key, Duration, Chord
>>> from pytheory.play import play_score
>>> key = Key("A", "minor")
>>> chords = key.random_progression(4)
>>> score = Score("4/4", bpm=140)
>>> score.add_pattern(Pattern.preset("bossa nova"), repeats=4)
>>> score = Pattern.preset("bossa nova").to_score(repeats=2, bpm=140)
>>> for chord in chords:
... score.add(chord, Duration.WHOLE)
... score.add(chord, Duration.WHOLE)
>>> score.save_midi("bossa_with_chords.mid")
>>> 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)
Each part has ``.add()`` and ``.rest()`` with fluent chaining. Parts accept
note strings directly — no need to wrap in ``Tone.from_string()``:
.. 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
>>> 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)
>>> play_score(score)
Available Synths and Envelopes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**Synths**: ``"sine"`` (pure, clean), ``"saw"`` (bright, brassy),
``"triangle"`` (mellow, woody)
**Envelopes**: ``"piano"`` (natural decay), ``"pluck"`` (sharp attack,
fast decay), ``"pad"`` (slow fade in, lush), ``"organ"`` (instant on/off),
``"bell"`` (instant attack, long ring), ``"strings"`` (gradual bow),
``"staccato"`` (short and punchy), ``"none"`` (raw waveform)
Full Example: Bossa Nova Arrangement
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A complete arrangement with drums, chord pads, walking bass, and
a triangle-wave melody:
.. code-block:: pycon
>>> from pytheory import Score, Pattern, Key, Duration, Chord
>>> from pytheory.play import play_score
>>> score = Score("4/4", bpm=140)
>>> score.add_pattern(Pattern.preset("bossa nova"), repeats=4)
>>> chords = score.part("chords", synth="sine", envelope="pad", volume=0.3)
>>> lead = score.part("lead", synth="triangle", envelope="pluck", volume=0.55)
>>> bass = score.part("bass", synth="sine", envelope="pluck", volume=0.4)
>>> # Chords: Am → Dm → E7 → Am (2 bars each)
>>> for sym in ["Am", "Am", "Dm", "Dm", "E7", "E7", "Am", "Am"]:
... chords.add(Chord.from_symbol(sym), Duration.WHOLE)
>>> # Lead: a lilting melody with swing 8ths
>>> 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)
>>> # Bass: root-fifth walking pattern
>>> for n in ["A2","E2","A2","C3","D2","A2","D2","F2"]:
... bass.add(n, Duration.QUARTER)
>>> play_score(score)
Headless Rendering
~~~~~~~~~~~~~~~~~~
Use ``render_score()`` to get a raw audio buffer without playing it —
useful for saving to WAV or further processing:
.. code-block:: pycon
>>> from pytheory.play import render_score
>>> buf = render_score(score) # numpy float32 array
>>> len(buf)
604800
Combining with MIDI Export
~~~~~~~~~~~~~~~~~~~~~~~~~~
Scores with parts can also be exported to MIDI (parts are rendered
to the default channel, drums to channel 10):
.. code-block:: pycon
>>> score.save_midi("bossa_arrangement.mid")
Drum Sounds
-----------
@@ -290,33 +384,44 @@ files needed.
>>> play_pattern(Pattern.preset("salsa"), repeats=4, bpm=180)
>>> play_pattern(Pattern.preset("afrobeat"), repeats=8, bpm=110)
Playing Drums with Chords
--------------------------
Playing Drums with Parts
-------------------------
``play_score()`` mixes tonal content and drum hits together into
one audio buffer. Build a Score from a drum pattern, then ``.add()``
chords on top:
``play_score()`` mixes drums and all named parts together. Use
``Score.part()`` to create voices with different timbres:
.. code-block:: pycon
>>> from pytheory import Pattern, Key, Duration
>>> from pytheory import Score, Pattern, Key, Duration, Chord
>>> from pytheory.play import play_score
>>> key = Key("A", "minor")
>>> score = Pattern.preset("bossa nova").to_score(repeats=4, bpm=140)
>>> for chord in key.progression("i", "iv", "V", "i"):
... score.add(chord, Duration.WHOLE)
... score.add(chord, Duration.WHOLE)
>>> score = Score("4/4", bpm=140)
>>> score.add_pattern(Pattern.preset("bossa nova"), repeats=4)
>>> chords = score.part("chords", synth="sine", envelope="pad", volume=0.3)
>>> lead = score.part("lead", synth="triangle", envelope="pluck", volume=0.5)
>>> for chord in Key("A", "minor").progression("i", "iv", "V", "i"):
... chords.add(chord, Duration.WHOLE)
>>> lead.add("E5", 0.67).add("D5", 0.33).add("C5", 1.0).rest(1.0)
>>> play_score(score)
Another example — salsa with a ii-V-I:
Another example — salsa with a saw lead and walking bass:
.. code-block:: pycon
>>> key = Key("C", "major")
>>> score = Pattern.preset("salsa").to_score(repeats=4, bpm=180)
>>> for chord in key.progression("ii", "V", "I", "I") * 2:
... score.add(chord, Duration.WHOLE)
>>> score = Score("4/4", bpm=180)
>>> score.add_pattern(Pattern.preset("salsa"), repeats=4)
>>> pads = score.part("pads", synth="sine", envelope="pad", volume=0.3)
>>> lead = score.part("lead", synth="saw", envelope="pluck", volume=0.4)
>>> bass = score.part("bass", synth="sine", envelope="pluck", volume=0.45)
>>> for chord in Key("D", "minor").progression("ii", "V", "i", "i") * 2:
... pads.add(chord, Duration.WHOLE)
>>> lead.add("A5", 0.67).add("G5", 0.33).add("F5", 0.67).add("E5", 0.33)
>>> for n in ["D2", "A2", "D2", "F2"] * 2:
... bass.add(n, Duration.QUARTER)
>>> play_score(score)
Synthesized Drum Sounds