"""Twelve-tone serialism — tone rows and the row matrix.
A **tone row** is simply an ordering of all twelve pitch classes
(C, C#, D, … B) in which each appears exactly once. It isn't a melody —
there's no rhythm — it's the raw pitch material a twelve-tone piece draws
on. By always running through all twelve before repeating any, no single
note can act as a tonal "home", which is how Schoenberg's method produces
atonal music on purpose.
From one row you derive **48 forms** via four operations, each at any of
the twelve transpositions:
- **Prime (P)** — the row as written.
- **Retrograde (R)** — the row backwards.
- **Inversion (I)** — every interval flipped upside-down.
- **Retrograde-Inversion (RI)** — the inversion, backwards.
Everything here is plain arithmetic on the numbers 0–11 (mod 12), so the
results are easy to verify: every form is still a permutation of all twelve
pitch classes, and every row and column of the matrix contains each pitch
class exactly once.
Example::
>>> row = ToneRow.from_names("C", "C#", "E", "D", "F", "D#",
... "F#", "A", "G#", "G", "B", "A#")
>>> row.P(0)[:4] # prime starting on C
[0, 1, 4, 2]
>>> row.I(0)[:4] # inversion starting on C — intervals mirrored
[0, 11, 8, 10]
>>> row.R(0) == row.P(0)[::-1]
True
"""
from __future__ import annotations
from typing import List
_NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F",
"F#", "G", "G#", "A", "A#", "B"]
_NAME_TO_PC = {}
for _pc, _nm in enumerate(_NOTE_NAMES):
_NAME_TO_PC[_nm] = _pc
# Accept flat spellings too.
_NAME_TO_PC.update({"Db": 1, "Eb": 3, "Gb": 6, "Ab": 8, "Bb": 10,
"Cb": 11, "Fb": 4, "E#": 5, "B#": 0})
def _pc_of(value) -> int:
"""Coerce a note name or number to a pitch class (0–11)."""
if isinstance(value, str):
name = value.strip()
if name not in _NAME_TO_PC:
raise ValueError(f"Unknown note name: {value!r}")
return _NAME_TO_PC[name]
return int(value) % 12
[docs]
class ToneRow:
"""A twelve-tone row and the 48 forms derived from it.
Construct from pitch-class numbers (0–11) or note names::
ToneRow([0, 1, 4, 2, 5, 3, 6, 9, 8, 7, 11, 10])
ToneRow.from_names("C", "C#", "E", "D", ...)
The transposition index ``n`` in :meth:`P`, :meth:`I`, :meth:`R`, and
:meth:`RI` is **the pitch class the form begins on** (the common
textbook convention). So ``P(0)`` starts on C, ``P(7)`` starts on G,
and the row you passed in is ``P(row[0])``.
"""
[docs]
def __init__(self, pitches):
pcs = [_pc_of(p) for p in pitches]
if len(pcs) != 12 or sorted(pcs) != list(range(12)):
raise ValueError(
"A tone row must list all 12 pitch classes exactly once; "
f"got {pitches!r}."
)
self.row: List[int] = pcs
# P0: the row transposed so it starts on pitch class 0.
self._p0 = [(pc - pcs[0]) % 12 for pc in pcs]
# I0: invert P0 around 0 (negate each interval from the start).
self._i0 = [(-pc) % 12 for pc in self._p0]
# ── Construction ──────────────────────────────────────────────────
[docs]
@classmethod
def from_names(cls, *names: str) -> "ToneRow":
"""Build a row from twelve note names, e.g.
``ToneRow.from_names("C", "C#", "E", ...)``."""
return cls(list(names))
# ── The four operations (each begins on pitch class n) ────────────
[docs]
def P(self, n: int = 0) -> List[int]:
"""**Prime** form beginning on pitch class ``n`` — the row,
transposed."""
return [(pc + n) % 12 for pc in self._p0]
[docs]
def I(self, n: int = 0) -> List[int]:
"""**Inversion** beginning on pitch class ``n`` — every interval of
the prime flipped upside-down."""
return [(pc + n) % 12 for pc in self._i0]
[docs]
def R(self, n: int = 0) -> List[int]:
"""**Retrograde** of ``P(n)`` — the prime form played backwards.
(It *ends* on pitch class ``n``, since it's ``P(n)`` reversed.)
"""
return self.P(n)[::-1]
[docs]
def RI(self, n: int = 0) -> List[int]:
"""**Retrograde-inversion** — ``I(n)`` played backwards."""
return self.I(n)[::-1]
# ── The matrix ────────────────────────────────────────────────────
[docs]
def matrix(self) -> List[List[int]]:
"""The 12×12 row matrix as a list of rows of pitch classes.
``P0`` runs along the top, ``I0`` down the left, and the top-left
to bottom-right diagonal is all zeros. Read a **row** left-to-right
for a Prime form, right-to-left for a Retrograde; read a **column**
top-to-bottom for an Inversion, bottom-to-top for a
Retrograde-Inversion.
"""
return [[(self._p0[j] + self._i0[i]) % 12 for j in range(12)]
for i in range(12)]
[docs]
def matrix_str(self, names: bool = True) -> str:
"""A printable matrix, labelled with the P/I/R/RI transpositions.
With ``names=True`` cells show note names; otherwise pitch-class
numbers. The left edge labels each row's Prime form and the right
edge its Retrograde; the top labels each column's Inversion and the
bottom its Retrograde-Inversion.
"""
grid = self.matrix()
cell = (lambda pc: _NOTE_NAMES[pc]) if names else (lambda pc: str(pc))
w = 4 # uniform column width keeps everything aligned
def fmt_row(values):
return "".join(f"{v:>{w}}" for v in values)
edge = " " * 5
lines = [edge + fmt_row([f"I{grid[0][j]}" for j in range(12)])]
for r in grid:
cells = fmt_row([cell(pc) for pc in r])
lines.append(f"{('P' + str(r[0])):>4} {cells} {'R' + str(r[0])}")
lines.append(edge + fmt_row([f"RI{grid[0][j]}" for j in range(12)]))
return "\n".join(lines)
# ── Names + analysis ──────────────────────────────────────────────
[docs]
def note_names(self, form: str = "P0") -> List[str]:
"""Note names for a given form label (default ``"P0"``)."""
return [_NOTE_NAMES[pc] for pc in self.form(form)]
@property
def interval_succession(self) -> List[int]:
"""The eleven ordered intervals (1–11 semitones) between successive
notes of the row."""
return [(self.row[k + 1] - self.row[k]) % 12 for k in range(11)]
@property
def is_all_interval(self) -> bool:
"""``True`` if this is an *all-interval row* — its eleven successive
intervals use each of the intervals 1–11 exactly once.
Example::
>>> ToneRow([0, 11, 1, 10, 2, 9, 3, 8, 4, 7, 5, 6]).is_all_interval
True
"""
return sorted(self.interval_succession) == list(range(1, 12))
[docs]
def __repr__(self) -> str:
return f"<ToneRow {' '.join(_NOTE_NAMES[pc] for pc in self.row)}>"
[docs]
def __eq__(self, other) -> bool:
return isinstance(other, ToneRow) and self.row == other.row
[docs]
def __len__(self) -> int:
return 12