Twelve-Tone Serialism ===================== `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 0–11. 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 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]) 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 (0–11): ``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 (1–11 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 `. ``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.