Files
kennethreitz 33b0636cc4 docs: comprehensive refresh for v0.57.x + sphinx-design hero
A full freshness + quality pass across the documentation, with every code
example verified by actually running it against v0.57.8.

Content / accuracy:
- Fixed stale and broken examples throughout: SymPy-based "symbolic pitch"
  (SymPy was removed), the case-sensitive progression parser (I/V/vi/IV vs
  i/iv), analog_drift -> analog, Key.relative/parallel returning Key objects,
  Tone.from_string validation needing a system, stale tab()/scale_diagram()
  output, and many counts (74 drum sounds, 100 patterns, 37 fills, 19 chord
  types, 16 systems, 25 fretboard instruments, 56 waveforms, 83 presets).
- Documented major recent features that were missing: Maqam (quarter-tone
  Arabic maqamat) and Raga (Hindustani + Carnatic, shruti just intonation)
  in the systems guide and the CLI; SVG/PNG diagram export; progression /
  cadence / secondary-dominant analysis and reharmonization; notation export
  to LilyPond/MusicXML/ABC incl. lyrics; from_wav transcription; render_scores
  batch rendering; CLI raga/maqam and `analyze song.mid`.

Navigation / presentation:
- Added sphinx-design + sphinx-copybutton to the docs deps and conf.
- Rebuilt the homepage "Why would I want this?" into a sphinx-design persona
  card grid with CTA buttons and a badge row; wired the brand logo into the
  sidebar (and suppressed the now-redundant project-name text).
- Added API-reference pages for the public Raga and Maqam classes.

Also: removed a duplicate `jati` attribute line from the Raga docstring so
autodoc no longer double-documents it.

Sphinx build is clean (0 warnings).
2026-06-28 18:12:45 -04:00

165 lines
6.3 KiB
ReStructuredText
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Twelve-Tone Serialism
=====================
`Twelve-tone technique <https://en.wikipedia.org/wiki/Twelve-tone_technique>`_
(serialism), devised by Arnold Schoenberg in the 1920s, is a way of
writing music in which **no note is allowed to feel like "home."** It
sounds intimidating, but the mechanism is one of the simplest in all of
music theory — it's just arithmetic on the numbers 011.
The tone row
------------
A **tone row** is an ordering of all twelve pitch classes (C, C#, D, … B)
in which each one appears exactly once. It isn't a melody — there's no
rhythm — it's the raw pitch material the piece is built from. Because you
cycle through all twelve before any can repeat, no note gets the emphasis
that would make it a tonic, and the music floats free of any key.
.. code-block:: pycon
>>> from pytheory import ToneRow
>>> row = ToneRow.from_names("C", "C#", "E", "D", "F", "D#",
... "F#", "A", "G#", "G", "B", "A#")
>>> row
<ToneRow C C# E D F D# F# A G# G B A#>
You can also build one straight from pitch-class numbers (``0`` = C):
.. code-block:: pycon
>>> ToneRow([0, 1, 4, 2, 5, 3, 6, 9, 8, 7, 11, 10])
<ToneRow C C# E D F D# F# A G# G B A#>
PyTheory checks that what you pass really is a row — all twelve pitch
classes, no repeats — so a typo can't slip through.
The four operations
-------------------
From a single row you derive **48 forms**: four operations, each at any of
the twelve transpositions. Every operation is just list arithmetic:
- **Prime (P)** — the row as written.
- **Retrograde (R)** — the row backwards.
- **Inversion (I)** — every interval flipped upside-down (a step up
becomes the same step down).
- **Retrograde-Inversion (RI)** — the inversion, backwards.
The number you pass is the **pitch class the form begins on**, so ``P(0)``
starts on C and ``P(7)`` starts on G. It defaults to ``0``, so a bare
``row.P()`` is shorthand for the prime form on C:
.. code-block:: pycon
>>> row.note_names("P0")
['C', 'C#', 'E', 'D', 'F', 'D#', 'F#', 'A', 'G#', 'G', 'B', 'A#']
>>> row.note_names("I0") # intervals mirrored
['C', 'B', 'G#', 'A#', 'G', 'A', 'F#', 'D#', 'E', 'F', 'C#', 'D']
>>> row.R(0) == row.P(0)[::-1] # retrograde really is "backwards"
True
Look up any form by label with ``row.form("P0")`` (also ``"I7"``, ``"R5"``,
``"RI11"``), or grab all 48 at once with ``row.all_forms()``.
The matrix
----------
The **row matrix** is a 12×12 grid that holds all 48 forms at once. ``P0``
runs across the top, ``I0`` down the left side, and the rest is filled in
by addition. To read it:
- a **row**, left → right, is a **Prime** form (right → left, a Retrograde);
- a **column**, top → bottom, is an **Inversion** (bottom → top, a
Retrograde-Inversion).
.. code-block:: pycon
>>> print(row.matrix_str())
I0 I1 I4 I2 I5 I3 I6 I9 I8 I7 I11 I10
P0 C C# E D F D# F# A G# G B A# R0
P11 B C D# C# E D F G# G F# A# A R11
P8 G# A C A# C# B D F E D# G F# R8
P10 A# B D C D# C# E G F# F A G# R10
P7 G G# B A C A# C# E D# D F# F R7
P9 A A# C# B D C D# F# F E G# G R9
P6 F# G A# G# B A C D# D C# F E R6
P3 D# E G F G# F# A C B A# D C# R3
P4 E F G# F# A G A# C# C B D# D R4
P5 F F# A G A# G# B D C# C E D# R5
P1 C# D F D# F# E G A# A G# C B R1
P2 D D# F# E G F G# B A# A C# C R2
RI0 RI1 RI4 RI2 RI5 RI3 RI6 RI9 RI8 RI7RI11RI10
Prefer integers to note names? Pass ``names=False`` to print the same grid
as pitch-class numbers (011): ``print(row.matrix_str(names=False))``.
Why you can trust it
--------------------
You don't have to take the theory on faith — the matrix is **self-checking**.
Because every form is a rearrangement of the same twelve numbers, three
properties must always hold, and PyTheory's tests assert exactly these:
.. code-block:: pycon
>>> m = row.matrix()
>>> all(sorted(r) == list(range(12)) for r in m) # every row complete
True
>>> all(sorted(c) == list(range(12)) for c in zip(*m)) # every column complete
True
>>> all(m[i][i] == 0 for i in range(12)) # diagonal all zeros
True
If any of those failed, the matrix would be wrong — so a glance confirms
it's right.
All-interval rows
-----------------
Some rows are special. An **all-interval row** uses each of the eleven
intervals (111 semitones) exactly once as it steps from note to note — a
prized bit of craftsmanship. ``row.is_all_interval`` checks for it, and
``row.interval_succession`` shows the intervals:
.. code-block:: pycon
>>> wedge = ToneRow([0, 11, 1, 10, 2, 9, 3, 8, 4, 7, 5, 6])
>>> wedge.interval_succession
[11, 2, 9, 4, 7, 6, 5, 8, 3, 10, 1]
>>> wedge.is_all_interval
True
From row to a piece
-------------------
A ``ToneRow`` deals only in pitch — it hands back pitch classes and note
names, with no rhythm, octave, or sound of its own. To actually *hear* a
row, feed its note names into a :doc:`Score <sequencing>`. ``note_names()``
returns octave-less names like ``"C"`` and ``"C#"``, so tack on an octave
to pin down each pitch:
.. code-block:: python
from pytheory import ToneRow, Score, Duration
from pytheory.play import play_score # for live playback
row = ToneRow.from_names("C", "C#", "E", "D", "F", "D#",
"F#", "A", "G#", "G", "B", "A#")
score = Score("4/4", bpm=120)
part = score.part("row", synth="sine", envelope="pluck")
for name in row.note_names("P0"):
part.add(f"{name}4", Duration.QUARTER) # the prime in octave 4
score.save_midi("row.mid") # write a Standard MIDI File
# play_score(score) # ...or hear it live
From there the whole toolkit opens up: layer P, I, R, and RI forms across
separate parts, give each its own rhythm, and render the result. See
:doc:`sequencing` for arranging multi-part scores, and :doc:`playback` for
live playback, MIDI, and notation export (ABC, LilyPond, MusicXML).
That's the whole technique. Pick twelve notes, agree never to favour one,
and let these four operations spin the material into a piece.