Source code for music21.serial

# ------------------------------------------------------------------------------
# Name:         serial.py
# Purpose:      music21 classes for serial transformations
#
# Authors:      Michael Scott Asato Cuthbert
#               Christopher Ariza
#
# Copyright:    Copyright © 2009-2012 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
This module defines objects for defining and manipulating structures
common to serial and/or twelve-tone music,
including :class:`~music21.serial.ToneRow` subclasses.

Serial searching methods that were previously here have been moved to
:mod:`~music21.search.serial`.
'''
from __future__ import annotations

import typing as t
import unittest

from music21 import chord
from music21 import environment
from music21 import exceptions21
from music21 import note
from music21 import pitch
from music21 import stream

environLocal = environment.Environment('serial')


# ------------------------------------------------------------------------------
class SerialException(exceptions21.Music21Exception):
    pass


# ------------------------------------------------------------------------------
[docs] class TwelveToneMatrix(stream.Stream): ''' An object representation of a 2-dimensional array of 12 pitches. Internal representation is as a :class:`~music21.stream.Stream`, which stores 12 Streams, each Stream a horizontal row of pitches in the matrix. This object is commonly used by calling the :meth:`~music21.stream.TwelveToneRow.matrix` method of :class:`~music21.stream.TwelveToneRow` (or a subclass). >>> ttr = serial.TwelveToneRow([0, 2, 11, 7, 8, 3, 9, 1, 4, 10, 6, 5]) >>> aMatrix = ttr.matrix() >>> print(aMatrix) 0 2 B 7 8 3 9 1 4 A 6 5 A 0 9 5 6 1 7 B 2 8 4 3 1 3 0 8 9 4 A 2 5 B 7 6 5 7 4 0 1 8 2 6 9 3 B A 4 6 3 B 0 7 1 5 8 2 A 9 9 B 8 4 5 0 6 A 1 7 3 2 3 5 2 A B 6 0 4 7 1 9 8 B 1 A 6 7 2 8 0 3 9 5 4 8 A 7 3 4 B 5 9 0 6 2 1 2 4 1 9 A 5 B 3 6 0 8 7 6 8 5 1 2 9 3 7 A 4 0 B 7 9 6 2 3 A 4 8 B 5 1 0 >>> repr(aMatrix) '<music21.serial.TwelveToneMatrix for [<music21.serial.TwelveToneRow row-1>]>' >>> fourthQuartetMatrix = serial.getHistoricalRowByName('SchoenbergOp37').matrix() >>> repr(fourthQuartetMatrix) '<music21.serial.TwelveToneMatrix for [<music21.serial.HistoricalTwelveToneRow Schoenberg Op. 37 Fourth String Quartet>]>' ''' def __str__(self): ''' Return a string representation of the matrix. ''' ret = [] for rowForm in self: msg = [] for n in rowForm: msg.append(str(n.pitch.pitchClassString).rjust(3)) ret.append(''.join(msg)) return '\n'.join(ret) def _reprInternal(self): if self: if isinstance(self[0], ToneRow): return f'for [{self[0]}]' else: return super()._reprInternal() else: return super()._reprInternal()
# ------------------------------------------------------------------------------ # noinspection SpellCheckingInspection historicalDict = { 'WebernOp29': ('Webern', 'Op. 29', 'Cantata I', [3, 11, 2, 1, 5, 4, 7, 6, 10, 9, 0, 8]), 'WebernOp28': ('Webern', 'Op. 28', 'String Quartet', [1, 0, 3, 2, 6, 7, 4, 5, 9, 8, 11, 10]), 'SchoenbergOp24Mvmt5': ('Schoenberg', 'Op. 24', 'Serenade, Mvt. 5, "Tanzscene"', [9, 10, 0, 3, 4, 6, 5, 7, 8, 11, 1, 2]), 'SchoenbergOp24Mvmt4': ('Schoenberg', 'Op. 24', 'Serenade, Mvt. 4, "Sonett"', [4, 2, 3, 11, 0, 1, 8, 6, 9, 5, 7, 10]), 'SchoenbergJakobsleiter': ('Schoenberg', None, 'Die Jakobsleiter', [1, 2, 5, 4, 8, 7, 0, 3, 11, 10, 6, 9]), 'SchoenbergOp27No4': ('Schoenberg', 'Op. 27 No. 4', 'Four Pieces for Mixed Chorus, No. 4', [1, 3, 10, 6, 8, 4, 11, 0, 2, 9, 5, 7]), 'WebernOp23': ('Webern', 'Op. 23', 'Three Songs', [8, 3, 7, 4, 10, 6, 2, 5, 1, 0, 9, 11]), 'BergLuluActIIScene1': ('Berg', 'Lulu, Act II, Scene 1', 'Perm. (Every 5th Note Of Transposed Primary Row)', [10, 7, 1, 0, 9, 2, 4, 11, 5, 8, 3, 6]), 'SchoenbergOp27No1': ('Schoenberg', 'Op. 27 No. 1', 'Four Pieces for Mixed Chorus, No. 1', [6, 5, 2, 8, 7, 1, 3, 4, 10, 9, 11, 0]), 'BergLuluActIScene20': ('Berg', 'Lulu, Act I , Scene XX', 'Perm. (Every 7th Note Of Transposed Primary Row)', [10, 6, 3, 8, 5, 11, 4, 2, 9, 0, 1, 7]), 'SchoenbergOp27No3': ('Schoenberg', 'Op. 27 No. 3', 'Four Pieces for Mixed Chorus, No. 3', [7, 6, 2, 4, 5, 3, 11, 0, 8, 10, 9, 1]), 'SchoenbergOp27No2': ('Schoenberg', 'Op. 27 No. 2', 'Four Pieces for Mixed Chorus, No. 2', [0, 11, 4, 10, 2, 8, 3, 7, 6, 5, 9, 1]), 'SchoenbergFragPiano': ('Schoenberg', None, 'Fragment For Piano', [6, 9, 0, 7, 1, 2, 8, 11, 5, 10, 4, 3]), 'SchoenbergOp50B': ('Schoenberg', 'Op. 50B', 'De Profundis', [3, 9, 8, 4, 2, 10, 7, 11, 0, 6, 5, 1]), 'SchoenbergOp50C': ('Schoenberg', 'Op. 50C', 'Modern Psalms, The First Psalm', [4, 3, 0, 8, 11, 7, 5, 9, 6, 10, 1, 2]), 'SchoenbergOp50A': ('Schoenberg', 'Op. 50A', 'Three Times A Thousand Years', [7, 9, 6, 4, 5, 11, 10, 2, 0, 1, 3, 8]), 'SchoenbergMosesAron': ('Schoenberg', None, 'Moses And Aron', [9, 10, 4, 2, 3, 1, 7, 5, 6, 8, 11, 0]), 'WebernOp25': ('Webern', 'Op. 25', 'Three Songs', [7, 4, 3, 6, 1, 5, 2, 11, 10, 0, 9, 8]), 'SchoenbergOp23No5': ('Schoenberg', 'Op. 23, No. 5', 'Five Piano Pieces', [1, 9, 11, 7, 8, 6, 10, 2, 4, 3, 0, 5]), 'SchoenbergOp28No1': ('Schoenberg', 'Op. 28 No. 1', 'Three Satires for Mixed Chorus, No. 1', [0, 4, 7, 1, 9, 11, 5, 3, 2, 6, 8, 10]), 'SchoenbergOp28No3': ('Schoenberg', 'Op. 28 No. 3', 'Three Satires for Mixed Chorus, No. 3', [5, 6, 4, 8, 2, 10, 7, 9, 3, 11, 1, 0]), 'WebernOp21': ('Webern', 'Op. 21', 'Chamber Symphony', [5, 8, 7, 6, 10, 9, 3, 4, 0, 1, 2, 11]), 'SchoenbergIsraelExists': ('Schoenberg', None, 'Israel Exists Again', [0, 3, 4, 9, 11, 5, 2, 1, 10, 8, 6, 7]), 'SchoenbergOp35No2': ('Schoenberg', 'Op. 35', 'Six Pieces for Male Chorus, No. 2', [6, 9, 7, 1, 0, 2, 5, 11, 10, 3, 4, 8]), 'SchoenbergOp35No3': ('Schoenberg', 'Op. 35', 'Six Pieces for Male Chorus, No. 3', [3, 6, 7, 8, 5, 0, 9, 10, 4, 11, 2, 1]), 'SchoenbergOp35No1': ('Schoenberg', 'Op. 35', 'Six Pieces for Male Chorus, No. 1', [2, 11, 3, 5, 4, 1, 8, 10, 9, 6, 0, 7]), 'SchoenbergOp48No1': ('Schoenberg', 'Op. 48', 'Three Songs, No. 1, "Sommermud"', [1, 2, 0, 6, 3, 5, 4, 10, 11, 7, 9, 8]), 'SchoenbergOp35No5': ('Schoenberg', 'Op. 35', 'Six Pieces for Male Chorus, No. 5', [1, 7, 10, 2, 3, 11, 8, 4, 0, 6, 5, 9]), 'SchoenbergOp29': ('Schoenberg', 'Op. 29', 'Suite', [3, 7, 6, 10, 2, 11, 0, 9, 8, 4, 5, 1]), 'BergLyricSuitePerm': ('Berg', None, 'Lyric Suite, Last Mvt. Permutation', [5, 6, 10, 4, 1, 9, 2, 8, 7, 3, 0, 11]), 'WebernOp20': ('Webern', 'Op. 20', 'String Trio', [8, 7, 2, 1, 6, 5, 9, 10, 3, 4, 0, 11]), 'SchoenbergOp46': ('Schoenberg', 'Op. 46', 'A Survivor From Warsaw', [6, 7, 0, 8, 4, 3, 11, 10, 5, 9, 1, 2]), 'SchoenbergFragOrganSonata': ('Schoenberg', None, 'Fragment of Sonata For Organ', [1, 7, 11, 3, 9, 2, 8, 6, 10, 5, 0, 4]), 'SchoenbergOp44': ('Schoenberg', 'Op. 44', 'Prelude To A Suite From "Genesis"', [10, 6, 2, 5, 4, 0, 11, 8, 1, 3, 9, 7]), 'SchoenbergOp45': ('Schoenberg', 'Op. 45', 'String Trio', [2, 10, 3, 9, 4, 1, 11, 8, 6, 7, 5, 0]), 'SchoenbergOp33A': ('Schoenberg', 'Op. 33A', 'Two Piano Pieces, No. 1', [10, 5, 0, 11, 9, 6, 1, 3, 7, 8, 2, 4]), 'SchoenbergOp25': ('Schoenberg', 'Op.25', 'Suite for Piano', [4, 5, 7, 1, 6, 3, 8, 2, 11, 0, 9, 10]), 'SchoenbergOp26': ('Schoenberg', 'Op. 26', 'Wind Quintet', [3, 7, 9, 11, 1, 0, 10, 2, 4, 6, 8, 5]), 'SchoenbergOp33B': ('Schoenberg', 'Op. 33B', 'Two Piano Pieces, No. 2', [11, 1, 5, 3, 9, 8, 6, 10, 7, 4, 0, 2]), 'BergViolinConcerto': ('Berg', None, 'Concerto For Violin And Orchestra', [7, 10, 2, 6, 9, 0, 4, 8, 11, 1, 3, 5]), 'WebernOp22': ('Webern', 'Op. 22', 'Quartet For Violin, Clarinet, Tenor Sax, And Piano', [6, 3, 2, 5, 4, 8, 9, 10, 11, 1, 7, 0]), 'BergLulu': ('Berg', None, 'Lulu: Primary Row', [0, 4, 5, 2, 7, 9, 6, 8, 11, 10, 3, 1]), 'WebernOp30': ('Webern', 'Op. 30', 'Variations For Orchestra', [9, 10, 1, 0, 11, 2, 3, 6, 5, 4, 7, 8]), 'WebernOp31': ('Webern', 'Op. 31', 'Cantata II', [6, 9, 5, 4, 8, 3, 7, 11, 10, 2, 1, 0]), 'WebernOpNo17No1': ('Webern', 'Op. 17, No. 1', '"Armer Sunder, Du"', [11, 10, 5, 6, 3, 4, 7, 8, 9, 0, 1, 2]), 'WebernOp24': ('Webern', 'Op. 24', 'Concerto For Nine Instruments', [11, 10, 2, 3, 7, 6, 8, 4, 5, 0, 1, 9]), 'SchoenbergOp48No2': ('Schoenberg', 'Op. 48', 'Three Songs, No. 2, "Tot"', [2, 3, 9, 1, 10, 4, 8, 7, 0, 11, 5, 6]), 'WebernOp27': ('Webern', 'Op. 27', 'Variations For Piano', [3, 11, 10, 2, 1, 0, 6, 4, 7, 5, 9, 8]), 'SchoenbergOp47': ('Schoenberg', 'Op. 47', 'Fantasy For Violin And Piano', [10, 9, 1, 11, 5, 7, 3, 4, 0, 2, 8, 6]), 'WebernOp19No2': ('Webern', 'Op. 19, No. 2', '"Ziehn Die Schafe"', [8, 4, 9, 6, 7, 0, 11, 5, 3, 2, 10, 1]), 'WebernOp19No1': ('Webern', 'Op. 19, No. 1', '"Weiss Wie Lilien"', [7, 10, 6, 5, 3, 9, 8, 1, 2, 11, 4, 0]), 'WebernOp26': ('Webern', 'Op. 26', 'Das Augenlicht', [8, 10, 9, 0, 11, 3, 4, 1, 5, 2, 6, 7]), 'SchoenbergFragPianoPhantasia': ('Schoenberg', None, 'Fragment of Phantasia For Piano', [1, 5, 3, 6, 4, 8, 0, 11, 2, 9, 10, 7]), 'BergDerWein': ('Berg', None, 'Der Wein', [2, 4, 5, 7, 9, 10, 1, 6, 8, 0, 11, 3]), 'BergWozzeckPassacaglia': ('Berg', None, 'Wozzeck, Act I, Scene 4 "Passacaglia"', [3, 11, 7, 1, 0, 6, 4, 10, 9, 5, 8, 2]), 'WebernOp18No1': ('Webern', 'Op. 18, No. 1', '"Schatzerl Klein"', [0, 11, 5, 8, 10, 9, 3, 4, 1, 7, 2, 6]), 'WebernOp18No2': ('Webern', 'Op. 18, No. 2', '"Erlosung"', [6, 9, 5, 8, 4, 7, 3, 11, 2, 10, 1, 0]), 'WebernOp18No3': ('Webern', 'Op. 18, No. 3', '"Ave, Regina Coelorum"', [4, 3, 7, 6, 5, 11, 10, 2, 1, 0, 9, 8]), 'SchoenbergOp42': ('Schoenberg', 'Op. 42', 'Concerto For Piano And Orchestra', [3, 10, 2, 5, 4, 0, 6, 8, 1, 9, 11, 7]), 'SchoenbergOp48No3': ('Schoenberg', 'Op. 48', 'Three Songs, No, 3, "Madchenlied"', [1, 7, 9, 11, 3, 5, 10, 6, 4, 0, 8, 2]), 'SchoenbergOp37': ('Schoenberg', 'Op. 37', 'Fourth String Quartet', [2, 1, 9, 10, 5, 3, 4, 0, 8, 7, 6, 11]), 'SchoenbergOp36': ('Schoenberg', 'Op. 36', 'Concerto for Violin and Orchestra', [9, 10, 3, 11, 4, 6, 0, 1, 7, 8, 2, 5]), 'SchoenbergOp34': ('Schoenberg', 'Op. 34', 'Accompaniment to a Film Scene', [3, 6, 2, 4, 1, 0, 9, 11, 10, 8, 5, 7]), 'BergChamberConcerto': ('Berg', None, 'Chamber Concerto', [11, 7, 5, 9, 2, 3, 6, 8, 0, 1, 4, 10]), 'SchoenbergOp32': ('Schoenberg', 'Op. 32', 'Von Heute Auf Morgen', [2, 3, 9, 1, 11, 5, 8, 7, 4, 0, 10, 6]), 'SchoenbergOp31': ('Schoenberg', 'Op. 31', 'Variations for Orchestra', [10, 4, 6, 3, 5, 9, 2, 1, 7, 8, 11, 0]), 'SchoenbergOp30': ('Schoenberg', 'Op. 30', 'Third String Quartet', [7, 4, 3, 9, 0, 5, 6, 11, 10, 1, 8, 2]), 'BergLyricSuite': ('Berg', None, 'Lyric Suite Primary Row', [5, 4, 0, 9, 7, 2, 8, 1, 3, 6, 10, 11]), 'SchoenbergOp41': ('Schoenberg', 'Op. 41', 'Ode To Napoleon', [1, 0, 4, 5, 9, 8, 3, 2, 6, 7, 11, 10]), 'WebernOp17No3': ('Webern', 'Op. 17, No. 3', '"Heiland, Unsere Missetaten..."', [8, 5, 4, 3, 7, 6, 0, 1, 2, 11, 10, 9]), 'WebernOp17No2': ('Webern', 'Op. 17, No. 2', '"Liebste Jungfrau"', [1, 0, 11, 7, 8, 2, 3, 6, 5, 4, 9, 10]) } # ------------------------------------------------------------------------------
[docs] class ToneRow(stream.Stream): ''' A Stream representation of a tone row, or an ordered sequence of pitches; can most importantly be used to deal with serial transformations. Unlike a normal Stream, the first argument is assumed to be a ToneRow: >>> toneRow = serial.ToneRow([10, 9, 4, 5, 6, 3, 2, 8, 7, 11, 0, 1]) The representation of a ToneRow will be the contents: >>> toneRow <music21.serial.ToneRow A94563287B01> Unless (like a Stream), the id is set: >>> toneRow.id = 'retrograde_jungfrau' >>> toneRow <music21.serial.ToneRow retrograde_jungfrau> A ToneRow does not need to have twelve pitches, like this ten-tone row from Ruth Crawford Seeger's String Quartet 1931. >>> rcsRow = serial.ToneRow([2, 4, 5, 3, 6, 9, 8, 7, 1, 0]) >>> rcsRow <music21.serial.ToneRow 2453698710> >>> len(rcsRow) 10 ''' _DOC_ATTR: dict[str, str] = { 'row': 'A list representing the pitch class values of the row.', } _DOC_ORDER = ['pitchClasses', 'noteNames', 'isTwelveToneRow', 'isSameRow', 'getIntervalsAsString', 'zeroCenteredTransformation', 'originalCenteredTransformation', 'findZeroCenteredTransformations', 'findOriginalCenteredTransformations'] def __init__(self, row=None, **keywords): super().__init__(**keywords) if row is not None: self.row = row else: self.row = [] for pc in self.row: if not isinstance(pc, (pitch.Pitch, note.Note)): pc = pitch.Pitch(pc) if not isinstance(pc, note.Note): n = note.Note(pitch=pc) else: n = pc n.pitch.octave = None self.append(n) def _reprInternal(self): superInternal = super()._reprInternal() if not superInternal.startswith('0x'): return superInternal return ''.join(n.pitch.pitchClassString for n in self)
[docs] def pitchClasses(self): ''' Convenience function showing the pitch classes of a :class:`~music21.serial.ToneRow` as a list. >>> fiveFold = [5 * i for i in range(12)] >>> fiveFold [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55] >>> quintupleRow = serial.pcToToneRow(fiveFold) >>> quintupleRow <music21.serial.TwelveToneRow 05A3816B4927> >>> quintupleRow.pitchClasses() [0, 5, 10, 3, 8, 1, 6, 11, 4, 9, 2, 7] >>> halfStep = serial.pcToToneRow([0, 1]) >>> halfStep.pitchClasses() [0, 1] ''' pitchList = [n.pitch.pitchClass for n in self] return pitchList
[docs] def noteNames(self): ''' Convenience function showing the note names of a :class:`~music21.serial.ToneRow` as a list. >>> chromatic = serial.TwelveToneRow(range(12)) >>> chromatic.noteNames() ['C', 'C#', 'D', 'E-', 'E', 'F', 'F#', 'G', 'G#', 'A', 'B-', 'B'] >>> halfStep = serial.ToneRow([0, 1]) >>> halfStep.noteNames() ['C', 'C#'] ''' noteList = [p.name for p in self] return noteList
[docs] def isTwelveToneRow(self): ''' Describes whether a :class:`~music21.serial.ToneRow` constitutes a twelve-tone row. Note that a :class:`~music21.serial.TwelveToneRow` object might not be a twelve-tone row. >>> serial.ToneRow([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]).isTwelveToneRow() True >>> serial.ToneRow([0, 4, 8]).isTwelveToneRow() False >>> serial.ToneRow([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]).isTwelveToneRow() False ''' pitchList = self.pitchClasses() if len(pitchList) != 12: return False else: temp = True for i in range(12): if i not in pitchList: temp = False return temp
[docs] def makeTwelveToneRow(self): # noinspection PyShadowingNames ''' Convenience function returning a :class:`~music21.serial.TwelveToneRow` with the same pitches. Note that a :class:`~music21.serial.ToneRow` may be created without being a true twelve tone row. >>> r1 = serial.pcToToneRow(range(11)) >>> type(r1) <class 'music21.serial.ToneRow'> >>> n = note.Note() >>> n.pitch.pitchClass = 11 >>> r1.append(n) >>> r2 = r1.makeTwelveToneRow() >>> type(r2) <class 'music21.serial.TwelveToneRow'> ''' pcSet = self.pitchClasses() a = TwelveToneRow() for thisPc in pcSet: n = note.Note() n.duration.quarterLength = 0.0 n.pitch.pitchClass = thisPc n.pitch.octave = None a.append(n) return a
[docs] def isSameRow(self, other): ''' Convenience function describing if two rows are the same. >>> row1 = serial.pcToToneRow([6, 7, 8]) >>> row2 = serial.pcToToneRow([-6, 19, 128]) >>> row3 = serial.pcToToneRow([6, 7, -8]) >>> row1.isSameRow(row2) True >>> row2.isSameRow(row1) True >>> row1.isSameRow(row3) False ''' if len(self) != len(other): return False for selfTone, otherTone in zip(self, other): if selfTone.pitch.pitchClass != otherTone.pitch.pitchClass: return False return True
[docs] def getIntervalsAsString(self): ''' Returns the string of intervals between consecutive pitch classes of a :class:`~music21.serial.ToneRow`. 'T' = 10, 'E' = 11. >>> cRow = serial.pcToToneRow([0]) >>> cRow.getIntervalsAsString() '' >>> reverseChromatic = serial.pcToToneRow([11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]) >>> reverseChromatic.getIntervalsAsString() 'EEEEEEEEEEE' ''' numPitches = len(self) pitchList = self.pitchClasses() intervalString = '' for i in range(numPitches - 1): interval = (pitchList[i + 1] - pitchList[i]) % 12 if interval in range(10): intervalString = intervalString + str(interval) if interval == 10: intervalString = intervalString + 'T' if interval == 11: intervalString = intervalString + 'E' return intervalString
[docs] def zeroCenteredTransformation(self, transformationType, index): ''' Returns a :class:`~music21.serial.ToneRow` giving a transformation of a tone row. Admissible transformationTypes are 'P' (prime), 'I' (inversion), 'R' (retrograde), and 'RI' (retrograde inversion). In the "zero-centered" convention, the transformations Pn and In start on the pitch class n, and the transformations Rn and RIn end on the pitch class n. >>> chromatic = serial.pcToToneRow(range(12)) >>> chromatic.pitchClasses() [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] >>> chromaticP3 = chromatic.zeroCenteredTransformation('P',3) >>> chromaticP3.pitchClasses() [3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2] >>> chromaticI6 = chromatic.zeroCenteredTransformation('I',6) >>> chromaticI6.pitchClasses() [6, 5, 4, 3, 2, 1, 0, 11, 10, 9, 8, 7] >>> schoenberg = serial.getHistoricalRowByName('SchoenbergOp26') >>> schoenberg.pitchClasses() [3, 7, 9, 11, 1, 0, 10, 2, 4, 6, 8, 5] >>> schoenbergR8 = schoenberg.zeroCenteredTransformation('R',8) >>> schoenbergR8.pitchClasses() [10, 1, 11, 9, 7, 3, 5, 6, 4, 2, 0, 8] >>> schoenbergRI9 = schoenberg.zeroCenteredTransformation('RI',9) >>> schoenbergRI9.noteNames() ['G', 'E', 'F#', 'G#', 'B-', 'D', 'C', 'B', 'C#', 'E-', 'F', 'A'] ''' numPitches = len(self) pitchList = self.pitchClasses() if int(index) != index: raise SerialException('Transformation must be by an integer.') firstPitch = pitchList[0] transformedPitchList = [] if transformationType in ('P', 'T'): for i in range(numPitches): newPitch = (pitchList[i] - firstPitch + index) % 12 transformedPitchList.append(newPitch) elif transformationType == 'I': for i in range(numPitches): newPitch = (index + firstPitch - pitchList[i]) % 12 transformedPitchList.append(newPitch) elif transformationType == 'R': for i in range(numPitches): newPitch = (index + pitchList[numPitches - 1 - i] - firstPitch) % 12 transformedPitchList.append(newPitch) elif transformationType == 'RI': for i in range(numPitches): newPitch = (index - pitchList[numPitches - 1 - i] + firstPitch) % 12 transformedPitchList.append(newPitch) else: raise SerialException(f'Invalid transformation type: {transformationType}') return pcToToneRow(transformedPitchList)
[docs] def originalCenteredTransformation(self, transformationType: str, index: int): ''' Returns a :class:`~music21.serial.ToneRow` giving a transformation of a tone row. Admissible transformations are 'T' (transposition), 'I' (inversion), 'R' (retrograde), and 'RI' (retrograde inversion). In the "original-centered" convention, which is less common than the "zero-centered" convention, the original row is not initially transposed to start on the pitch class 0. Thus, the transformation Tn transposes the original row up by n semitones, and the transformations In, Rn, and RIn first transform the row appropriately (without transposition), then transpose the resulting row by n semitones. >>> chromatic = serial.pcToToneRow(range(12)) >>> chromatic.pitchClasses() [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] >>> chromaticP3 = chromatic.originalCenteredTransformation('T',3) >>> chromaticP3.pitchClasses() [3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2] >>> chromaticI6 = chromatic.originalCenteredTransformation('I',6) >>> chromaticI6.pitchClasses() [6, 5, 4, 3, 2, 1, 0, 11, 10, 9, 8, 7] >>> schoenberg = serial.getHistoricalRowByName('SchoenbergOp26') >>> schoenberg.pitchClasses() [3, 7, 9, 11, 1, 0, 10, 2, 4, 6, 8, 5] >>> schoenbergR8 = schoenberg.originalCenteredTransformation('R',8) >>> schoenbergR8.pitchClasses() [1, 4, 2, 0, 10, 6, 8, 9, 7, 5, 3, 11] >>> schoenbergRI9 = schoenberg.originalCenteredTransformation('RI',9) >>> schoenbergRI9.noteNames() ['B-', 'G', 'A', 'B', 'C#', 'F', 'E-', 'D', 'E', 'F#', 'G#', 'C'] ''' pitchList = self.pitchClasses() firstPitch = pitchList[0] newIndex = (firstPitch + index) % 12 if transformationType in ('P', 'T'): return self.zeroCenteredTransformation('P', newIndex) return self.zeroCenteredTransformation(transformationType, newIndex)
[docs] def findZeroCenteredTransformations(self, otherRow) -> t.Union[bool, list[t.Any]]: ''' Gives the list of zero-centered serial transformations taking one :class:`~music21.serial.ToneRow` to another, the second specified in the argument. Each transformation is given as a tuple of the transformation type and index. See :meth:`~music21.serial.ToneRow.zeroCenteredTransformation` for an explanation of this convention. >>> chromatic = serial.pcToToneRow([2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1]) >>> reverseChromatic = serial.pcToToneRow([8, 7, 6, 5, 4, 3, 2, 1, 0, 11, 10, 9]) >>> chromatic.findZeroCenteredTransformations(reverseChromatic) [('I', 8), ('R', 9)] >>> schoenberg25 = serial.getHistoricalRowByName('SchoenbergOp25') >>> schoenberg26 = serial.pcToToneRow(serial.getHistoricalRowByName( ... 'SchoenbergOp26').row) >>> schoenberg25.findZeroCenteredTransformations(schoenberg26) [] >>> schoenberg26.findZeroCenteredTransformations( ... schoenberg26.zeroCenteredTransformation('RI', 8)) [('RI', 8)] ''' if len(self) != len(otherRow): return False otherRowPitches = otherRow.pitchClasses() transformationList = [] firstPitch = otherRowPitches[0] lastPitch = otherRowPitches[-1] if otherRowPitches == self.zeroCenteredTransformation('P', firstPitch).pitchClasses(): transformation = 'P', firstPitch transformationList.append(transformation) if otherRowPitches == self.zeroCenteredTransformation('I', firstPitch).pitchClasses(): transformation = 'I', firstPitch transformationList.append(transformation) if otherRowPitches == self.zeroCenteredTransformation('R', lastPitch).pitchClasses(): transformation = 'R', lastPitch transformationList.append(transformation) if otherRowPitches == self.zeroCenteredTransformation('RI', lastPitch).pitchClasses(): transformation = 'RI', lastPitch transformationList.append(transformation) return transformationList
[docs] def findOriginalCenteredTransformations(self, otherRow): ''' Gives the list of original-centered serial transformations taking one :class:`~music21.serial.ToneRow` to another, the second specified in the argument. Each transformation is given as a tuple of the transformation type and index. See :meth:`~music21.serial.ToneRow.originalCenteredTransformation` for an explanation of this convention. >>> chromatic = serial.pcToToneRow( [2, 3, 4, 5, 6, 7, 8, 9, 'A', 'B', 0, 1]) >>> reverseChromatic = serial.pcToToneRow([8, 7, 6, 5, 4, 3, 2, 1, 0, 'B', 'A', 9]) >>> chromatic.findOriginalCenteredTransformations(reverseChromatic) [('I', 6), ('R', 7)] >>> schoenberg25 = serial.getHistoricalRowByName('SchoenbergOp25') >>> schoenberg26 = serial.getHistoricalRowByName('SchoenbergOp26') >>> schoenberg25.findOriginalCenteredTransformations(schoenberg26) [] >>> schoenberg26.findOriginalCenteredTransformations( ... schoenberg26.originalCenteredTransformation('RI',8)) [('RI', 8)] ''' # This is rather confused in execution. originalRowPitches = self.pitchClasses() otherRowPitches = otherRow.pitchClasses() transformationList = [] oldFirstPitch = originalRowPitches[0] oldLastPitch = originalRowPitches[-1] newFirstPitch = otherRowPitches[0] # for T, I: tiPitch = (newFirstPitch - oldFirstPitch) % 12 # for R rPitch = (newFirstPitch - oldLastPitch) % 12 # for RI riPitch = (newFirstPitch - (2 * oldFirstPitch) + oldLastPitch) % 12 # bound method ocTrans = self.originalCenteredTransformation if otherRowPitches == ocTrans('T', tiPitch).pitchClasses(): transformation = ('T', tiPitch) transformationList.append(transformation) if otherRowPitches == ocTrans('I', tiPitch).pitchClasses(): transformation = ('I', tiPitch) transformationList.append(transformation) if otherRowPitches == ocTrans('R', rPitch).pitchClasses(): transformation = ('R', rPitch) transformationList.append(transformation) if otherRowPitches == ocTrans('RI', riPitch).pitchClasses(): transformation = ('RI', riPitch) transformationList.append(transformation) return transformationList
# ----------------------------------------------------------------------------------------
[docs] class TwelveToneRow(ToneRow): ''' A Stream representation of a twelve-tone row, capable of producing a 12-tone matrix. ''' _DOC_ORDER = ['matrix', 'isAllInterval', 'getLinkClassification', 'isLinkChord', 'areCombinatorial']
[docs] def matrix(self): # noinspection PyShadowingNames ''' Returns a :class:`~music21.serial.TwelveToneMatrix` object for the row. That object can just be printed (or displayed via .show()). >>> src = serial.getHistoricalRowByName('SchoenbergOp37') >>> [p.name for p in src] ['D', 'C#', 'A', 'B-', 'F', 'E-', 'E', 'C', 'G#', 'G', 'F#', 'B'] >>> len(src) 12 >>> s37 = serial.getHistoricalRowByName('SchoenbergOp37').matrix() >>> print(s37) 0 B 7 8 3 1 2 A 6 5 4 9 1 0 8 9 4 2 3 B 7 6 5 A 5 4 0 1 8 6 7 3 B A 9 2 4 3 B 0 7 5 6 2 A 9 8 1 ... >>> [str(e.pitch) for e in s37[0]] ['C', 'B', 'G', 'G#', 'E-', 'C#', 'D', 'B-', 'F#', 'F', 'E', 'A'] ''' # note: do not want to return a TwelveToneRow() type, as this will # add again the same pitches to the elements list twice. noteList = self.getElementsByClass(note.Note) i = [(12 - x.pitch.pitchClass) % 12 for x in noteList] matrix = [[(x.pitch.pitchClass + trans) % 12 for x in noteList] for trans in i] matrixObj = TwelveToneMatrix() i = 0 for row in matrix: i += 1 rowObject = self.__class__() rowObject.mergeAttributes(self) rowObject.id = 'row-' + str(i) for p in row: # iterate over pitch class values n = note.Note() n.duration.quarterLength = 0.0 n.pitch.pitchClass = p n.pitch.octave = None rowObject.append(n) matrixObj.insert(0, rowObject) # environLocal.printDebug([ # 'calling matrix start: len row:', self.row, 'len self', len(self)]) return matrixObj
[docs] def findHistorical(self): ''' Checks if a given :class:`music21.serial.TwelveToneRow` is the same as any of the historical twelve-tone rows stored by music21: see :func:`music21.serial.getHistoricalRowByName`. Returns a list of names of historical rows to which the input row is identical. >>> row = serial.pcToToneRow([2, 3, 9, 1, 11, 5, 8, 7, 4, 0, 10, 6]) >>> row.findHistorical() ['SchoenbergOp32'] >>> chromatic = serial.pcToToneRow(range(12)) >>> chromatic.findHistorical() [] ''' sameRows = [] for historicalRow in historicalDict: if self.isSameRow(getHistoricalRowByName(historicalRow)): sameRows.append(historicalRow) return sameRows
[docs] def findTransformedHistorical(self, convention): ''' Checks if a given :class:`music21.serial.TwelveToneRow` is a transformation of any historical twelve-tone row stored by music21 (see :func:`music21.serial.getHistoricalRowByName`). Returns a list of tuples, the tuple consisting of the name of a historical row, and a list of transformations relating the input row to the historical row. The convention for serial transformations must also be specified as 'zero' or 'original', as explained in :meth:`~music21.serial.ToneRow.findZeroCenteredTransformations` and :meth:`~music21.serial.ToneRow.findOriginalCenteredTransformations`. >>> row = serial.pcToToneRow([5, 9, 11, 3, 6, 7, 4, 10, 0, 8, 2, 1]) >>> row.findTransformedHistorical('original') [('SchoenbergOp32', [('R', 11)])] ''' sameRows = [] if convention == 'zero': for historicalRow in historicalDict: trans = getHistoricalRowByName(historicalRow).findZeroCenteredTransformations(self) if trans: sameRows.append((historicalRow, trans)) return sameRows if convention == 'original': for historicalRow in historicalDict: historicalRowObject = getHistoricalRowByName(historicalRow) trans = historicalRowObject.findOriginalCenteredTransformations(self) if trans: sameRows.append((historicalRow, trans)) return sameRows else: raise SerialException("Invalid convention - choose 'zero' or 'original'.")
[docs] def isAllInterval(self) -> bool: ''' Describes whether a :class:`~music21.serial.TwelveToneRow` is an all-interval row. >>> chromatic = serial.pcToToneRow(range(12)) >>> chromatic.pitchClasses() [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] >>> chromatic.isAllInterval() False >>> bergLyric = serial.getHistoricalRowByName('BergLyricSuite') >>> bergLyric.pitchClasses() [5, 4, 0, 9, 7, 2, 8, 1, 3, 6, 10, 11] >>> bergLyric.isAllInterval() True ''' if self.isTwelveToneRow() is False: raise SerialException('An all-interval row must be a twelve-tone row.') tempAllInterval = True intervalString = self.getIntervalsAsString() for i in range(1, 10): if str(i) not in intervalString: tempAllInterval = False if 'T' not in intervalString: tempAllInterval = False if 'E' not in intervalString: tempAllInterval = False return tempAllInterval
[docs] def getLinkClassification(self): ''' Gives the classification number of a Link Chord (as given in http://www.johnlinkmusic.com/LinkChords.pdf), that is, is an all-interval twelve-tone row containing a voicing of the all-trichord hexachord: [0, 1, 2, 4, 7, 8]. In addition, gives a list of sets of five contiguous intervals within the row representing a voicing of the all-trichord hexachord. Note that the interval sets may be transformed. Named for John Link who discovered them. >>> bergLyric = serial.getHistoricalRowByName('BergLyricSuite') >>> bergLyric.pitchClasses() [5, 4, 0, 9, 7, 2, 8, 1, 3, 6, 10, 11] >>> bergLyric.isAllInterval() True >>> bergLyric.getLinkClassification() (None, []) >>> link = serial.pcToToneRow([0, 3, 8, 2, 10, 11, 9, 4, 1, 5, 7, 6]) >>> link.getLinkClassification() (62, ['8352E']) >>> doubleLink = serial.pcToToneRow([0, 1, 8, 5, 7, 10, 4, 3, 11, 9, 2, 6]) >>> doubleLink.getLinkClassification() (33, ['236E8', '36E8T']) ''' # the link interval strings given below are untransformed: by inversion around 0, # original-centered retrograde, and # retrograde inversion around zero, we get three more link interval strings for # each one in fullLinkIntervals. # in the original rows, this corresponds simply to checking the interval string of any # inversion, retrograde inversion, and retrograde, respectively. fullLinkIntervals = ['125634T97E8', '134E78526T9', '134E79T6258', '134E79T6258', '1367T89E254', '137E542896T', '137E982456T', '142965837ET', '142973856ET', '1429738E65T', '14297TE6853', '145638E729T', '1456T729E83', '1456T982E73', '145927E836T', '14598E63T72', '14689T7E253', '1469E27T853', '149278356ET', '1492783E65T', '1496258T73E', '1496E358T72', '14972836E5T', '14972E6385T', '1497T853E62', '14E379T6528', '14E6T783529', '172E6853T94', '17356ET8294', '1738T542E69', '173E65T8294', '1763T4952E8', '176852E34T9', '179236E8T54', '179236E8T54', '17923E685T4', '179245T8E63', '17924T586E3', '179T65234E8', '179T8E63254', '179T8E63254', '1825E43796T', '1825E43796T', '1825E79T364', '1825E79T364', '1852E43769T', '1852E79463T', '185629TE743', '18563479TE2', '18563E7T492', '18734E5296T', '18734E5296T', '187E259436T', '187E259436T', '18T352E7964', '18T497E3562', '18T97E52364', '18T97E52364', '18E63T79452', '18E63T79452', '19476538TE2', '1947T8536E2', '1954763T8E2', '19742538E6T', '197425T6E83', '1974T8E6352', '197T8532E64', '1T5E7928364', '1T63E874259', '1T6E3852479', '1T8352E4679', '1T974253E68', '214367TE985', '214376598ET', '214376598ET', '2143E86597T', '2149586E37T', '214976538ET', '214976538ET', '216734ET985', '216743E9T85', '217634TE985', '21T8E956347', '21T96583E47', '234T1596E87', '235189E647T', '235189E647T', '23689E7T145', '236E981547T', '236E981547T', '23T514697E8', '2513647ET98', '25189TE7463', '25189TE7463', '2546E981T73', '25691T8E473', '2569E8T1437', '258T73E6149', '258T9614E37', '25T31496E87', '25T89E61437', '2618T497E35', '26347ET9185', '263891T7E45', '263891T7E45', '2653E718T49', '2654T1783E9', '2654T1783E9', '2654E3871T9', '2654E3871T9', '2654E7T1983', '2654E7T1983', '265819TE743', '2659E8T1347', '267431T8E95', '2694T817E35', '269T1783E45', '269T1783E45', '269E3871T45', '269E3871T45', '26E451T7389', '26E451T7389', '26E459817T3', '26E459817T3', '26E4T718953', '26E4T718953', '26E873T5149', '26E95134T87', '26E95178T43', '274316E985T', '274316E985T', '2743E86591T', '274916E385T', '274916E385T', '2749586E31T', '276134ET985', '276143E9T85', '27E34169T85', '2965387E41T', '2965387E41T', '296E387T145', '29TE7436185', '29TE7463158', '29E7835641T', '29E7835641T', '2E431T96587', '2E431T96587', '2E465387T19', '2E637T85419', '2E783T51469', '2E783T51469', '2E796415T38', '2E8T1956743', '2E9658T4137', '3142E8956T7', '3142ET79685', '31456T972E8', '314672E9T85', '3152689TE74', '3152689TE74', '3152E9T8764', '3158629TE74', '3158629TE74', '316452E98T7', '317ET562894', '317ET926854', '317ET986254', '319765T42E8', '3198265TE74', '31T524796E8', '31T567942E8', '31T7E294685', '325E79T8164', '325E79T8164', '3265981TE74', '329E71T4568', '329E71T4568', '32E981T6547', '347621T8E95', '3479TE26185', '34T9E712568', '35146T927E8', '35146T927E8', '351T64927E8', '351T64927E8', '351T7924E68', '35216E98T74', '3521T8E9674', '3581629ET74', '3594T6127E8', '359E6128T74', '35E7216T498', '35E72946T18', '35E729T6418', '3625189TE74', '3625189TE74', '36524ET8917', '3674218T9E5', '36T154927E8', '36T154927E8', '3764128T9E5', '38297E5T164', '38E729T6145', '3T17E924568', '3T17E924568', '3T4952E8617', '3T6194527E8', '3T62E815497', '3T97E528164', '3T97E528164', '3E2418596T7', '3E7T4926185', '416352E7T98', '41T629E7835', '41T629E7835', '4328T56E917', '4328TE65917', '43T9E865217', '463152E9T87', '46529E8T137', '46731T8E925', '4692E513T87', '4692E513T87', '46982315ET7', '469T315E287', '469T315E287', '4769E251T38', '4783T1629E5', '47T198236E5', '47E928361T5', '4T1629E3785', '4TE86592317', '4E2538T1697', '4E29658T317', '4E29658T317', '4ET85692317', '4ET85692317', '5896T142E37'] specialLinkIntervals = [ '25634', '134E7', '134E7', 'E79T6', '89E25', '7E542', '7E982', '29658', '856ET', '8E65T', 'E6853', '8E729', '729E8', '982E7', '927E8', '8E63T', '7E253', '7T853', '78356', '783E6', '58T73', '358T7', '2836E', '2E638', '7T853', '79T65', '78352', 'E6853', '56ET8', '42E69', 'E65T8', '4952E', '52E34', '236E8', '36E8T', '3E685', 'T8E63', '586E3', '79T65', 'T8E63', '8E632', '25E43', '5E437', '25E79', '5E79T', 'E4376', 'E7946', '9TE74', '479TE', 'E7T49', '734E5', '34E52', '87E25', 'E2594', '352E7', 'T497E', 'T97E5', '97E52', '8E63T', 'T7945', '76538', '7T853', '4763T', '97425', '97425', 'T8E63', '7T853', '5E792', '74259', '52479', '8352E', '97425', '4367T', '43765', '76598', 'E8659', '586E3', '49765', '76538', '6734E', '21674', '7634T', '1T8E9', 'T9658', '34T15', '5189E', '189E6', '689E7', '6E981', 'E9815', '3T514', '47ET9', '25189', '9TE74', '6E981', '91T8E', '9E8T1', '58T73', 'T9614', '5T314', '89E61', 'T497E', '47ET9', '891T7', '1T7E4', 'E718T', 'T1783', '1783E', 'E3871', '3871T', '4E7T1', '7T198', '9TE74', '9E8T1', '1T8E9', 'T817E', 'T1783', '1783E', 'E3871', '3871T', '451T7', '1T738', '59817', '9817T', 'T7189', '71895', '3T514', '5134T', '5178T', '4316E', '16E98', 'E8659', '4916E', '16E38', '586E3', '6134E', '27614', '4169T', '65387', '5387E', '6E387', '9TE74', '9TE74', 'E7835', '78356', 'E431T', 'T9658', '65387', '37T85', '2E783', '3T514', '415T3', 'E8T19', '9658T', '3142E', '3142E', '56T97', '31467', '52689', '9TE74', '152E9', '58629', '9TE74', '52E98', '56289', '92685', '98625', '765T4', '98265', '52479', '56794', '31T7E', '25E79', '5E79T', '65981', '29E71', '9E71T', '2E981', '1T8E9', '479TE', 'T9E71', '146T9', '927E8', '1T649', '927E8', '51T79', '16E98', '1T8E9', '1629E', '94T61', 'E6128', '16T49', '946T1', '9T641', '25189', '9TE74', '36524', '36742', 'T1549', '927E8', '37641', '297E5', '8E729', 'T17E9', '17E92', '4952E', '61945', '15497', 'T97E5', '97E52', '3E241', 'E7T49', '352E7', '629E7', 'E7835', '8T56E', '8TE65', '9E865', '152E9', '9E8T1', '1T8E9', '2E513', 'E513T', '2315E', 'T315E', '315E2', '9E251', '1629E', '7T198', '8361T', '1629E', 'E8659', 'E2538', '29658', '9658T', 'T8569', '85692', '142E3'] linkClassification = [ # line breaks emphasize repetitions 1, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 33, 34, 35, 36, 37, 38, 38, 39, 39, 40, 40, 41, 42, 43, 44, 45, 46, 46, 47, 47, 48, 49, 50, 50, 51, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 65, 66, 67, 68, 68, 69, 70, 71, 72, 73, 74, 75, 75, 76, 77, 77, 78, 79, 80, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 90, 91, 92, 92, 93, 93, 94, 94, 95, 96, 97, 98, 99, 99, 100, 100, 101, 101, 102, 102, 103, 103, 104, 105, 106, 107, 107, 108, 109, 109, 110, 111, 112, 113, 114, 114, 115, 116, 117, 118, 118, 119, 119, 120, 121, 122, 122, 123, 124, 125, 126, 127, 128, 129, 130, 130, 131, 132, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 142, 143, 144, 144, 145, 146, 147, 148, 149, 149, 150, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 160, 161, 162, 163, 163, 164, 165, 166, 167, 167, 168, 169, 170, 171, 171, 172, 173, 174, 175, 175, 176, 177, 178, 179, 180, 181, 182, 182, 183, 184, 184, 185, 186, 187, 188, 189, 190, 191, 192, 192, 193, 193, 194] numChords = len(fullLinkIntervals) if self.isTwelveToneRow() is False: raise SerialException('A Link Chord must be a twelve-tone row.') rowChecklist = [self, self.zeroCenteredTransformation('I', 0), self.zeroCenteredTransformation('R', 0), self.zeroCenteredTransformation('RI', 0)] specialIntervals = [] classification = None for row in rowChecklist: intervals = row.getIntervalsAsString() for i in range(numChords): if fullLinkIntervals[i] == intervals: classification = linkClassification[i] specialIntervals.append(specialLinkIntervals[i]) if not specialIntervals: return None, [] else: return classification, specialIntervals
[docs] def isLinkChord(self) -> bool: ''' Describes whether a :class:`~music21.serial.TwelveToneRow` is a Link Chord. >>> bergLyric = serial.getHistoricalRowByName('BergLyricSuite') >>> bergLyric.pitchClasses() [5, 4, 0, 9, 7, 2, 8, 1, 3, 6, 10, 11] >>> bergLyric.isAllInterval() True >>> bergLyric.isLinkChord() False >>> link = serial.pcToToneRow([0, 3, 8, 2, 10, 11, 9, 4, 1, 5, 7, 6]) >>> link.isLinkChord() True >>> doubleLink = serial.pcToToneRow([0, 1, 8, 5, 7, 10, 4, 3, 11, 9, 2, 6]) >>> doubleLink.isLinkChord() True ''' linkTuple = self.getLinkClassification() if linkTuple[0] is None: return False else: return True
[docs] def areCombinatorial(self, transType1: str, index1: int, transType2: str, index2: int) -> bool: ''' Describes whether two transformations of a twelve-tone row are combinatorial. The first and second arguments describe one transformation, while the third and fourth describe another. First, let's take a row we know to have a combinatoriality pair: >>> moses = serial.getHistoricalRowByName('SchoenbergMosesAron') >>> moses.pitchClasses() [9, 10, 4, 2, 3, 1, 7, 5, 6, 8, 11, 0] Combinatoriality holds here between P0 and I3 >>> moses.areCombinatorial('P', 0, 'I', 3) True And a combinatorial pair like this between P0 and I3 will also hold if you modify both rows in the same way, e.g. if you transpose both by the same amount >>> moses.areCombinatorial('P', 1, 'I', 4) True or if you retrograde both >>> moses.areCombinatorial('R', 1, 'RI', 4) True Any modification made to one row form and not the other means all bets are off >>> moses.areCombinatorial('R', 6, 'RI', 4) False * Changed in v7: `convention` is no longer necessary and no longer used. Renamed to `unused_convention` and defaults None; to be removed in v8. ''' if self.isTwelveToneRow() is False: raise SerialException('Combinatoriality applies only to twelve-tone rows.') # choice of convention does not matter trans1 = self.zeroCenteredTransformation(transType1, index1) trans2 = self.zeroCenteredTransformation(transType2, index2) pitches1 = trans1.pitchClasses() pitches2 = trans2.pitchClasses() testRow = pitches1[:6] + pitches2[:6] return pcToToneRow(testRow).isTwelveToneRow()
[docs] class HistoricalTwelveToneRow(TwelveToneRow): ''' Subclass of :class:`~music21.serial.TwelveToneRow` storing additional attributes of a twelve-tone row used in the historical literature. ''' _DOC_ATTR: dict[str, str] = { 'composer': 'The name of the composer, or None. (String)', 'opus': 'The opus of the work, or None. (String)', 'title': 'The title of the work, or None. (String)', } composer: None|str = None opus: None|str = None title: None|str = None def __init__(self, composer: None|str = None, opus: None|str = None, title: None|str = None, row=None, **keywords): super().__init__(row, **keywords) self.composer = composer self.opus = opus self.title = title
[docs] def mergeAttributes(self, other): super().mergeAttributes(other) if not isinstance(other, HistoricalTwelveToneRow): return self.composer = other.composer self.opus = other.opus self.title = other.title
def _reprInternal(self): return f'{self.composer} {self.opus} {self.title}'
[docs] def getHistoricalRowByName(rowName): ''' Given the name referring to a twelve-tone row used in the historical literature, returns a :class:`~music21.serial.HistoricalTwelveToneRow` object with attributes describing the row. The names of the rows with stored attributes are below (each must be passed as a string, in single quotes). >>> for r in sorted(list(serial.historicalDict)): ... print(r) BergChamberConcerto BergDerWein BergLulu BergLuluActIIScene1 BergLuluActIScene20 BergLyricSuite BergLyricSuitePerm BergViolinConcerto BergWozzeckPassacaglia SchoenbergFragOrganSonata SchoenbergFragPiano SchoenbergFragPianoPhantasia SchoenbergIsraelExists SchoenbergJakobsleiter SchoenbergMosesAron SchoenbergOp23No5 SchoenbergOp24Mvmt4 SchoenbergOp24Mvmt5 SchoenbergOp25 SchoenbergOp26 SchoenbergOp27No1 SchoenbergOp27No2 SchoenbergOp27No3 SchoenbergOp27No4 SchoenbergOp28No1 SchoenbergOp28No3 SchoenbergOp29 SchoenbergOp30 SchoenbergOp31 SchoenbergOp32 SchoenbergOp33A SchoenbergOp33B SchoenbergOp34 SchoenbergOp35No1 SchoenbergOp35No2 SchoenbergOp35No3 SchoenbergOp35No5 SchoenbergOp36 SchoenbergOp37 SchoenbergOp41 SchoenbergOp42 SchoenbergOp44 SchoenbergOp45 SchoenbergOp46 SchoenbergOp47 SchoenbergOp48No1 SchoenbergOp48No2 SchoenbergOp48No3 SchoenbergOp50A SchoenbergOp50B SchoenbergOp50C WebernOp17No2 WebernOp17No3 WebernOp18No1 WebernOp18No2 WebernOp18No3 WebernOp19No1 WebernOp19No2 WebernOp20 WebernOp21 WebernOp22 WebernOp23 WebernOp24 WebernOp25 WebernOp26 WebernOp27 WebernOp28 WebernOp29 WebernOp30 WebernOp31 WebernOpNo17No1 >>> web = serial.getHistoricalRowByName('WebernOp29') >>> web.row [3, 11, 2, 1, 5, 4, 7, 6, 10, 9, 0, 8] >>> web.composer 'Webern' >>> web.opus 'Op. 29' >>> web.title 'Cantata I' >>> web.isLinkChord() False NOTE: before v6, these rows had 'Row' in front of them, like 'RowWebernOp29' instead of 'WebernOp29'. They can still be accessed by the old name. >>> rowWeb = serial.getHistoricalRowByName('RowWebernOp29') >>> rowWeb.row == web.row True ''' if rowName.startswith('Row'): rowName = rowName[3:] if rowName in historicalDict: attr = historicalDict[rowName] rowObj = HistoricalTwelveToneRow(attr[0], attr[1], attr[2], attr[3]) return rowObj else: raise SerialException('No historical row with given name found')
# ------------------------------------------------------------------------------
[docs] def pcToToneRow(pcSet): # noinspection GrazieInspection ''' A convenience function that, given a list of pitch classes represented as integers and turns it into a :class:`~music21.serial.ToneRow` object. >>> chromaticRow = serial.pcToToneRow([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) >>> chromaticRow.show('text') {0.0} <music21.note.Note C> {1.0} <music21.note.Note C#> {2.0} <music21.note.Note D> {3.0} <music21.note.Note E-> {4.0} <music21.note.Note E> {5.0} <music21.note.Note F> {6.0} <music21.note.Note F#> {7.0} <music21.note.Note G> {8.0} <music21.note.Note G#> {9.0} <music21.note.Note A> {10.0} <music21.note.Note B-> {11.0} <music21.note.Note B> >>> matrixObj = chromaticRow.matrix() >>> print(matrixObj) 0 1 2 3 4 5 6 7 8 9 A B B 0 1 2 3 4 5 6 7 8 9 A ... >>> fancyRow = serial.pcToToneRow([4, 5, 0, 6, 7, 2, 'a', 8, 9, 1, 'b', 3]) >>> matrixObj = fancyRow.matrix() >>> print(matrixObj) 0 1 8 2 3 A 6 4 5 9 7 B B 0 7 1 2 9 5 3 4 8 6 A ... Note that the Row does not have to be a valid row to exist: >>> multiSetRow = serial.pcToToneRow([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) >>> multiSetRow.pitchClasses() [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] Or even have 12 notes: >>> shortRow = serial.pcToToneRow([3, 4]) >>> shortRow.pitchClasses() [3, 4] If the row does have twelve notes (whether unique or not) the object returned is a `TwelveToneRow`: >>> multiSetRow <music21.serial.TwelveToneRow 111111111111> Otherwise, it is simply a `ToneRow`: >>> shortRow <music21.serial.ToneRow 34> ''' if len(pcSet) == 12: a = TwelveToneRow() else: a = ToneRow() for thisPc in pcSet: n = note.Note() n.pitch.pitchClass = thisPc n.pitch.octave = None a.append(n) return a
[docs] def rowToMatrix(p: list[int]) -> str: # noinspection PyShadowingNames ''' Takes a list of numbers and converts it to a string representation of a 12-tone matrix. >>> aMatrix = serial.rowToMatrix([0, 2, 11, 7, 8, 3, 9, 1, 4, 10, 6, 5]) >>> print(aMatrix) 0 2 11 7 8 3 9 1 4 10 6 5 10 0 9 5 6 1 7 11 2 8 4 3 1 3 0 8 9 4 10 2 5 11 7 6 5 7 4 0 1 8 2 6 9 3 11 10 4 6 3 11 0 7 1 5 8 2 10 9 9 11 8 4 5 0 6 10 1 7 3 2 3 5 2 10 11 6 0 4 7 1 9 8 11 1 10 6 7 2 8 0 3 9 5 4 8 10 7 3 4 11 5 9 0 6 2 1 2 4 1 9 10 5 11 3 6 0 8 7 6 8 5 1 2 9 3 7 10 4 0 11 7 9 6 2 3 10 4 8 11 5 1 0 This function survives today (2020) because it's been an example function for `music21` since the very first demonstrations of predecessors from around 2000 onwards. Nowadays, better to create a `TwelveToneRow()` object and then get a matrix from that: >>> ttr = serial.TwelveToneRow([0, 2, 11, 7, 8, 3, 9, 1, 4, 10, 6, 5]) >>> matrix = ttr.matrix() >>> print(matrix) 0 2 B 7 8 3 9 1 4 A 6 5 A 0 9 5 6 1 7 B 2 8 4 3 1 3 0 8 9 4 A 2 5 B 7 6 5 7 4 0 1 8 2 6 9 3 B A 4 6 3 B 0 7 1 5 8 2 A 9 9 B 8 4 5 0 6 A 1 7 3 2 3 5 2 A B 6 0 4 7 1 9 8 B 1 A 6 7 2 8 0 3 9 5 4 8 A 7 3 4 B 5 9 0 6 2 1 2 4 1 9 A 5 B 3 6 0 8 7 6 8 5 1 2 9 3 7 A 4 0 B 7 9 6 2 3 A 4 8 B 5 1 0 ''' i = [(12 - x) % 12 for x in p] matrix = [[(x + trans) % 12 for x in p] for trans in i] ret = [] for row in matrix: msg = [] for thisPC in row: msg.append(str(thisPC).rjust(3)) ret.append(''.join(msg)) return '\n'.join(ret)
# ------------------------------------------------------------------------------ class Test(unittest.TestCase): def testMatrix(self): src = getHistoricalRowByName('SchoenbergOp37') self.assertEqual([p.name for p in src], ['D', 'C#', 'A', 'B-', 'F', 'E-', 'E', 'C', 'G#', 'G', 'F#', 'B']) s37 = getHistoricalRowByName('SchoenbergOp37').matrix() r0 = s37[0] self.assertEqual([e.name for e in r0], ['C', 'B', 'G', 'G#', 'E-', 'C#', 'D', 'B-', 'F#', 'F', 'E', 'A']) def testLabelingA(self): from music21 import corpus series = {'a': 1, 'g-': 2, 'g': 3, 'a-': 4, 'f': 5, 'e-': 6, 'e': 7, 'd': 8, 'c': 9, 'c#': 10, 'b-': 11, 'b': 12} s = corpus.parse('bwv66.6') for n in s.flatten().notes: for key in series: if n.pitch.pitchClass == pitch.Pitch(key).pitchClass: n.addLyric(series[key]) match = [] for n in s.parts[0].flatten().notes: match.append(n.lyric) self.assertEqual(match, ['10', '12', '1', '12', '10', '7', '10', '12', '1', '10', '1', '12', '4', '2', '1', '12', '12', '2', '7', '1', '12', '10', '10', '1', '12', '10', '1', '4', '2', '4', '2', '2', '2', '2', '2', '5', '2']) # s.show() def testHistorical(self): nonRows = [] for historicalRow in historicalDict: if getHistoricalRowByName(historicalRow).isTwelveToneRow() is False: nonRows.append(historicalRow) self.assertEqual(nonRows, []) def testExtractRowParts(self): ''' Was a problem in slices ''' aRow = getHistoricalRowByName('BergViolinConcerto') unused_aRow2 = aRow[0:3] def testPostTonalDocs(self): aRow = getHistoricalRowByName('BergViolinConcerto') # aMatrix = aRow.matrix() bStream = stream.Stream() for i in range(0, 12, 3): aRow2 = aRow[i:i + 3] c = chord.Chord(aRow2) c.addLyric(c.primeFormString) c.addLyric(c.forteClass) bStream.append(c) def testCopyAndDeepcopy(self): from music21.test.commonTest import testCopyAll testCopyAll(self, globals()) # def testRows(self): # from music21 import interval # # self.assertEqual(len(vienneseRows), 71) # # totalRows = 0 # cRows = 0 # for thisRow in vienneseRows: # thisRow = thisRow() # self.assertIsInstance(thisRow, TwelveToneRow) # # if thisRow.composer == 'Berg': # continue # post = thisRow.title # # totalRows += 1 # if thisRow[0].pitchClass == 0: # cRows += 1 # # if interval.Interval(thisRow[0], # thisRow[6]).intervalClass == 6: # # between element 1 and element 7 is there a TriTone? # rowsWithTTRelations += 1 # ------------------------------------------------------------------------------ # define presented order in documentation _DOC_ORDER = [ToneRow, TwelveToneRow, HistoricalTwelveToneRow, pcToToneRow, TwelveToneMatrix, rowToMatrix, getHistoricalRowByName, ] if __name__ == '__main__': import music21 music21.mainTest(Test)