Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bb811cf1c | |||
| a85898b416 | |||
| 640a1440b3 | |||
| 64dd35e1f8 | |||
| 42d4cfb427 | |||
| 6c42477d7a | |||
| 929813c587 | |||
| b0e0fe94ee |
8 changed files with 894 additions and 145 deletions
11
README.md
11
README.md
|
|
@ -25,17 +25,20 @@ In Python:
|
||||||
from setclass import SetClass
|
from setclass import SetClass
|
||||||
sc = SetClass(0, 3, 5, 6, 7, 10, 11) # Forte 7-20; pitch classes as integers 0-11
|
sc = SetClass(0, 3, 5, 6, 7, 10, 11) # Forte 7-20; pitch classes as integers 0-11
|
||||||
sc.versions
|
sc.versions
|
||||||
sc.brightest_form
|
sc.rahn_normal_form
|
||||||
sc.darkest_form
|
sc.forte_name
|
||||||
sc.duodecimal_notation # SetClass[0,3,5,6,7,T,E]
|
sc.duodecimal_notation # SetClass[0,3,5,6,7,T,E]
|
||||||
```
|
```
|
||||||
|
|
||||||
Proper library documentation to come soon with Sphinx.
|
Documentation is generated from the doc comments with Sphinx, and in the meantime is available [here](https://git.jon.geek.nz/docs/public/setclass/setclass.html):
|
||||||
|
|
||||||
|
```
|
||||||
|
make -C docs html
|
||||||
|
```
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- Documentation (Sphinx)
|
- <s>Documentation (Sphinx)</s>
|
||||||
- Interoperate with music21 objects
|
- Interoperate with music21 objects
|
||||||
- Generate MIDI files
|
- Generate MIDI files
|
||||||
- Generate LilyPond files for set pitches
|
- Generate LilyPond files for set pitches
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
|
|
||||||
# You can set these variables from the command line, and also
|
# You can set these variables from the command line, and also
|
||||||
# from the environment for the first two.
|
# from the environment for the first two.
|
||||||
|
PROJROOT := $(abspath $(MAKEFILE_LIST)/../..)
|
||||||
SPHINXOPTS ?=
|
SPHINXOPTS ?=
|
||||||
SPHINXBUILD ?= sphinx-build
|
SPHINXBUILD ?= PYTHONPATH="$(PROJROOT)" sphinx-build
|
||||||
SOURCEDIR = source
|
SOURCEDIR = source
|
||||||
BUILDDIR = build
|
BUILDDIR = build
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ release = '0.1'
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
extensions = [
|
extensions = [
|
||||||
|
'sphinx.ext.napoleon',
|
||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
'sphinx.ext.todo',
|
'sphinx.ext.todo',
|
||||||
'sphinx.ext.viewcode',
|
'sphinx.ext.viewcode',
|
||||||
|
|
@ -61,9 +62,7 @@ source_suffix = {
|
||||||
# -- Options for HTML output -------------------------------------------------
|
# -- Options for HTML output -------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
html_theme = 'sphinx_rtd_theme'
|
html_theme = 'sphinx_rtd_theme'
|
||||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
|
||||||
html_theme_options = {
|
html_theme_options = {
|
||||||
'display_version': False,
|
|
||||||
'navigation_depth': 2,
|
'navigation_depth': 2,
|
||||||
'prev_next_buttons_location': 'None'
|
'prev_next_buttons_location': 'None'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ setclass module
|
||||||
|
|
||||||
.. automodule:: setclass.setclass
|
.. automodule:: setclass.setclass
|
||||||
:members:
|
:members:
|
||||||
|
:special-members: __init__
|
||||||
|
:member-order: bysource
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# Main requirements
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
# Code lint
|
# Code lint
|
||||||
|
|
@ -14,5 +15,6 @@ tox
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
sphinx
|
sphinx
|
||||||
sphinx_rtd_theme
|
sphinx-rtd-theme
|
||||||
|
sphinxcontrib-napoleon
|
||||||
myst-parser
|
myst-parser
|
||||||
|
|
|
||||||
353
setclass/forte.py
Normal file
353
setclass/forte.py
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
FORTE_PRIMES = {
|
||||||
|
1: '1-1',
|
||||||
|
3: '2-1',
|
||||||
|
5: '2-2',
|
||||||
|
9: '2-3',
|
||||||
|
17: '2-4',
|
||||||
|
33: '2-5',
|
||||||
|
65: '2-6',
|
||||||
|
7: '3-1',
|
||||||
|
11: '3-2',
|
||||||
|
13: '3-2',
|
||||||
|
19: '3-3',
|
||||||
|
25: '3-3',
|
||||||
|
35: '3-4',
|
||||||
|
49: '3-4',
|
||||||
|
67: '3-5',
|
||||||
|
97: '3-5',
|
||||||
|
21: '3-6',
|
||||||
|
37: '3-7',
|
||||||
|
41: '3-7',
|
||||||
|
69: '3-8',
|
||||||
|
81: '3-8',
|
||||||
|
133: '3-9',
|
||||||
|
73: '3-10',
|
||||||
|
137: '3-11',
|
||||||
|
145: '3-11',
|
||||||
|
273: '3-12',
|
||||||
|
15: '4-1',
|
||||||
|
23: '4-2',
|
||||||
|
29: '4-2',
|
||||||
|
27: '4-3',
|
||||||
|
39: '4-4',
|
||||||
|
57: '4-4',
|
||||||
|
71: '4-5',
|
||||||
|
113: '4-5',
|
||||||
|
135: '4-6',
|
||||||
|
51: '4-7',
|
||||||
|
99: '4-8',
|
||||||
|
195: '4-9',
|
||||||
|
45: '4-10',
|
||||||
|
43: '4-11',
|
||||||
|
53: '4-11',
|
||||||
|
77: '4-12',
|
||||||
|
89: '4-12',
|
||||||
|
75: '4-13',
|
||||||
|
105: '4-13',
|
||||||
|
141: '4-14',
|
||||||
|
177: '4-14',
|
||||||
|
83: '4-Z15',
|
||||||
|
101: '4-Z15',
|
||||||
|
163: '4-16',
|
||||||
|
197: '4-16',
|
||||||
|
153: '4-17',
|
||||||
|
147: '4-18',
|
||||||
|
201: '4-18',
|
||||||
|
275: '4-19',
|
||||||
|
281: '4-19',
|
||||||
|
291: '4-20',
|
||||||
|
85: '4-21',
|
||||||
|
149: '4-22',
|
||||||
|
169: '4-22',
|
||||||
|
165: '4-23',
|
||||||
|
277: '4-24',
|
||||||
|
325: '4-25',
|
||||||
|
297: '4-26',
|
||||||
|
293: '4-27',
|
||||||
|
329: '4-27',
|
||||||
|
585: '4-28',
|
||||||
|
139: '4-Z29',
|
||||||
|
209: '4-Z29',
|
||||||
|
31: '5-1',
|
||||||
|
47: '5-2',
|
||||||
|
61: '5-2',
|
||||||
|
55: '5-3',
|
||||||
|
59: '5-3',
|
||||||
|
79: '5-4',
|
||||||
|
121: '5-4',
|
||||||
|
143: '5-5',
|
||||||
|
241: '5-5',
|
||||||
|
103: '5-6',
|
||||||
|
115: '5-6',
|
||||||
|
199: '5-7',
|
||||||
|
227: '5-7',
|
||||||
|
93: '5-8',
|
||||||
|
87: '5-9',
|
||||||
|
117: '5-9',
|
||||||
|
91: '5-10',
|
||||||
|
109: '5-10',
|
||||||
|
157: '5-11',
|
||||||
|
185: '5-11',
|
||||||
|
107: '5-Z12',
|
||||||
|
279: '5-13',
|
||||||
|
285: '5-13',
|
||||||
|
167: '5-14',
|
||||||
|
229: '5-14',
|
||||||
|
327: '5-15',
|
||||||
|
155: '5-16',
|
||||||
|
217: '5-16',
|
||||||
|
283: '5-Z17',
|
||||||
|
179: '5-Z18',
|
||||||
|
205: '5-Z18',
|
||||||
|
203: '5-19',
|
||||||
|
211: '5-19',
|
||||||
|
355: '5-20',
|
||||||
|
397: '5-20',
|
||||||
|
307: '5-21',
|
||||||
|
409: '5-21',
|
||||||
|
403: '5-22',
|
||||||
|
173: '5-23',
|
||||||
|
181: '5-23',
|
||||||
|
171: '5-24',
|
||||||
|
213: '5-24',
|
||||||
|
301: '5-25',
|
||||||
|
361: '5-25',
|
||||||
|
309: '5-26',
|
||||||
|
345: '5-26',
|
||||||
|
299: '5-27',
|
||||||
|
425: '5-27',
|
||||||
|
333: '5-28',
|
||||||
|
357: '5-28',
|
||||||
|
331: '5-29',
|
||||||
|
421: '5-29',
|
||||||
|
339: '5-30',
|
||||||
|
405: '5-30',
|
||||||
|
587: '5-31',
|
||||||
|
589: '5-31',
|
||||||
|
595: '5-32',
|
||||||
|
613: '5-32',
|
||||||
|
341: '5-33',
|
||||||
|
597: '5-34',
|
||||||
|
661: '5-35',
|
||||||
|
151: '5-Z36',
|
||||||
|
233: '5-Z36',
|
||||||
|
313: '5-Z37',
|
||||||
|
295: '5-Z38',
|
||||||
|
457: '5-Z38',
|
||||||
|
63: '6-1',
|
||||||
|
95: '6-2',
|
||||||
|
125: '6-2',
|
||||||
|
111: '6-Z3',
|
||||||
|
123: '6-Z3',
|
||||||
|
119: '6-Z4',
|
||||||
|
207: '6-5',
|
||||||
|
243: '6-5',
|
||||||
|
231: '6-Z6',
|
||||||
|
455: '6-7',
|
||||||
|
189: '6-8',
|
||||||
|
175: '6-9',
|
||||||
|
245: '6-9',
|
||||||
|
187: '6-Z10',
|
||||||
|
221: '6-Z10',
|
||||||
|
183: '6-Z11',
|
||||||
|
237: '6-Z11',
|
||||||
|
215: '6-Z12',
|
||||||
|
235: '6-Z12',
|
||||||
|
219: '6-Z13',
|
||||||
|
315: '6-14',
|
||||||
|
441: '6-14',
|
||||||
|
311: '6-15',
|
||||||
|
473: '6-15',
|
||||||
|
371: '6-16',
|
||||||
|
413: '6-16',
|
||||||
|
407: '6-Z17',
|
||||||
|
467: '6-Z17',
|
||||||
|
423: '6-18',
|
||||||
|
459: '6-18',
|
||||||
|
411: '6-Z19',
|
||||||
|
435: '6-Z19',
|
||||||
|
819: '6-20',
|
||||||
|
349: '6-21',
|
||||||
|
373: '6-21',
|
||||||
|
343: '6-22',
|
||||||
|
469: '6-22',
|
||||||
|
365: '6-Z23',
|
||||||
|
347: '6-Z24',
|
||||||
|
437: '6-Z24',
|
||||||
|
363: '6-Z25',
|
||||||
|
429: '6-Z25',
|
||||||
|
427: '6-Z26',
|
||||||
|
603: '6-27',
|
||||||
|
621: '6-27',
|
||||||
|
619: '6-Z28',
|
||||||
|
717: '6-Z29',
|
||||||
|
715: '6-30',
|
||||||
|
845: '6-30',
|
||||||
|
691: '6-31',
|
||||||
|
821: '6-31',
|
||||||
|
693: '6-32',
|
||||||
|
685: '6-33',
|
||||||
|
725: '6-33',
|
||||||
|
683: '6-34',
|
||||||
|
853: '6-34',
|
||||||
|
1365: '6-35',
|
||||||
|
159: '6-Z36',
|
||||||
|
249: '6-Z36',
|
||||||
|
287: '6-Z37',
|
||||||
|
399: '6-Z38',
|
||||||
|
317: '6-Z39',
|
||||||
|
377: '6-Z39',
|
||||||
|
303: '6-Z40',
|
||||||
|
489: '6-Z40',
|
||||||
|
335: '6-Z41',
|
||||||
|
485: '6-Z41',
|
||||||
|
591: '6-Z42',
|
||||||
|
359: '6-Z43',
|
||||||
|
461: '6-Z43',
|
||||||
|
615: '6-Z44',
|
||||||
|
627: '6-Z44',
|
||||||
|
605: '6-Z45',
|
||||||
|
599: '6-Z46',
|
||||||
|
629: '6-Z46',
|
||||||
|
663: '6-Z47',
|
||||||
|
669: '6-Z47',
|
||||||
|
679: '6-Z48',
|
||||||
|
667: '6-Z49',
|
||||||
|
723: '6-Z50',
|
||||||
|
127: '7-1',
|
||||||
|
191: '7-2',
|
||||||
|
253: '7-2',
|
||||||
|
319: '7-3',
|
||||||
|
505: '7-3',
|
||||||
|
223: '7-4',
|
||||||
|
251: '7-4',
|
||||||
|
239: '7-5',
|
||||||
|
247: '7-5',
|
||||||
|
415: '7-6',
|
||||||
|
499: '7-6',
|
||||||
|
463: '7-7',
|
||||||
|
487: '7-7',
|
||||||
|
381: '7-8',
|
||||||
|
351: '7-9',
|
||||||
|
501: '7-9',
|
||||||
|
607: '7-10',
|
||||||
|
637: '7-10',
|
||||||
|
379: '7-11',
|
||||||
|
445: '7-11',
|
||||||
|
671: '7-Z12',
|
||||||
|
375: '7-13',
|
||||||
|
477: '7-13',
|
||||||
|
431: '7-14',
|
||||||
|
491: '7-14',
|
||||||
|
471: '7-15',
|
||||||
|
623: '7-16',
|
||||||
|
635: '7-16',
|
||||||
|
631: '7-Z17',
|
||||||
|
755: '7-Z18',
|
||||||
|
829: '7-Z18',
|
||||||
|
719: '7-19',
|
||||||
|
847: '7-19',
|
||||||
|
743: '7-20',
|
||||||
|
925: '7-20',
|
||||||
|
823: '7-21',
|
||||||
|
827: '7-21',
|
||||||
|
871: '7-22',
|
||||||
|
701: '7-23',
|
||||||
|
757: '7-23',
|
||||||
|
687: '7-24',
|
||||||
|
981: '7-24',
|
||||||
|
733: '7-25',
|
||||||
|
749: '7-25',
|
||||||
|
699: '7-26',
|
||||||
|
885: '7-26',
|
||||||
|
695: '7-27',
|
||||||
|
949: '7-27',
|
||||||
|
747: '7-28',
|
||||||
|
861: '7-28',
|
||||||
|
727: '7-29',
|
||||||
|
941: '7-29',
|
||||||
|
855: '7-30',
|
||||||
|
939: '7-30',
|
||||||
|
731: '7-31',
|
||||||
|
877: '7-31',
|
||||||
|
859: '7-32',
|
||||||
|
875: '7-32',
|
||||||
|
1367: '7-33',
|
||||||
|
1371: '7-34',
|
||||||
|
1387: '7-35',
|
||||||
|
367: '7-Z36',
|
||||||
|
493: '7-Z36',
|
||||||
|
443: '7-Z37',
|
||||||
|
439: '7-Z38',
|
||||||
|
475: '7-Z38',
|
||||||
|
255: '8-1',
|
||||||
|
383: '8-2',
|
||||||
|
509: '8-2',
|
||||||
|
639: '8-3',
|
||||||
|
447: '8-4',
|
||||||
|
507: '8-4',
|
||||||
|
479: '8-5',
|
||||||
|
503: '8-5',
|
||||||
|
495: '8-6',
|
||||||
|
831: '8-7',
|
||||||
|
927: '8-8',
|
||||||
|
975: '8-9',
|
||||||
|
765: '8-10',
|
||||||
|
703: '8-11',
|
||||||
|
1013: '8-11',
|
||||||
|
763: '8-12',
|
||||||
|
893: '8-12',
|
||||||
|
735: '8-13',
|
||||||
|
1005: '8-13',
|
||||||
|
759: '8-14',
|
||||||
|
957: '8-14',
|
||||||
|
863: '8-Z15',
|
||||||
|
1003: '8-Z15',
|
||||||
|
943: '8-16',
|
||||||
|
983: '8-16',
|
||||||
|
891: '8-17',
|
||||||
|
879: '8-18',
|
||||||
|
987: '8-18',
|
||||||
|
887: '8-19',
|
||||||
|
955: '8-19',
|
||||||
|
951: '8-20',
|
||||||
|
1375: '8-21',
|
||||||
|
1391: '8-22',
|
||||||
|
1403: '8-22',
|
||||||
|
1455: '8-23',
|
||||||
|
1399: '8-24',
|
||||||
|
1495: '8-25',
|
||||||
|
1467: '8-26',
|
||||||
|
1463: '8-27',
|
||||||
|
1499: '8-27',
|
||||||
|
1755: '8-28',
|
||||||
|
751: '8-Z29',
|
||||||
|
989: '8-Z29',
|
||||||
|
511: '9-1',
|
||||||
|
767: '9-2',
|
||||||
|
1021: '9-2',
|
||||||
|
895: '9-3',
|
||||||
|
1019: '9-3',
|
||||||
|
959: '9-4',
|
||||||
|
1015: '9-4',
|
||||||
|
991: '9-5',
|
||||||
|
1007: '9-5',
|
||||||
|
1407: '9-6',
|
||||||
|
1471: '9-7',
|
||||||
|
1531: '9-7',
|
||||||
|
1503: '9-8',
|
||||||
|
1527: '9-8',
|
||||||
|
1519: '9-9',
|
||||||
|
1759: '9-10',
|
||||||
|
1775: '9-11',
|
||||||
|
1783: '9-11',
|
||||||
|
1911: '9-12',
|
||||||
|
1023: '10-1',
|
||||||
|
1535: '10-2',
|
||||||
|
1791: '10-3',
|
||||||
|
1919: '10-4',
|
||||||
|
1983: '10-5',
|
||||||
|
2015: '10-6',
|
||||||
|
2047: '11-1',
|
||||||
|
4095: '12-1',
|
||||||
|
}
|
||||||
|
|
@ -1,37 +1,37 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from functools import cached_property
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
class cache_property:
|
|
||||||
"""
|
|
||||||
Property that only computes its value once when first accessed, and caches the result.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, function):
|
|
||||||
self.function = function
|
|
||||||
self.name = function.__name__
|
|
||||||
self.__doc__ = function.__doc__
|
|
||||||
|
|
||||||
def __get__(self, obj, type=None) -> object:
|
|
||||||
obj.__dict__[self.name] = self.function(obj)
|
|
||||||
return obj.__dict__[self.name]
|
|
||||||
|
|
||||||
|
|
||||||
class SetClass(list):
|
class SetClass(list):
|
||||||
"""
|
"""
|
||||||
Musical set class, containing zero or more pitch classes.
|
Musical set class, containing zero or more pitch classes. This implementation can handle set
|
||||||
|
classes of any arbitrary :attr:`tonality`, or number of uniform divisions of the octave.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args: int, tonality: int = 12) -> None:
|
def __init__(self, *args: int, tonality: int = 12) -> None:
|
||||||
"""
|
"""
|
||||||
Instantiate a Class Set with a series of integers. Each pitch class is "normalised" by
|
Instantiate a :class:`ClassSet` object with a series of integers.
|
||||||
modulo the tonality; the set is sorted in ascending order; and values "rotated" until the
|
|
||||||
lowest value is 0. For a number of divisions of the octave other than the default (Western
|
|
||||||
harmony) value of 12, supply an integer value using the 'tonality' keyword. Example:
|
|
||||||
|
|
||||||
sc = SetClass(0, 7, 9)
|
Each supplied pitch class is "normalised" by modulo the :attr:`tonality`, the set is
|
||||||
sc = SetClass(0, 2, 3, tonality=7)
|
sorted in ascending order, and values are transposed until the lowest pitch class value is
|
||||||
|
0. For a number of divisions of the octave other than the default (Western harmony) value of
|
||||||
|
12, supply an integer value using the 'tonality' keyword. Example:
|
||||||
|
|
||||||
|
>>> sc1 = SetClass(0, 7, 9)
|
||||||
|
>>> sc1
|
||||||
|
SetClass{0,7,9}
|
||||||
|
>>> sc2 = SetClass(0, 2, 3, tonality=7)
|
||||||
|
>>> sc2
|
||||||
|
SetClass{0,2,3} (T=7)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*args (int):
|
||||||
|
The pitch class values.
|
||||||
|
tonality (int):
|
||||||
|
The :attr:`tonality`, or modulus, is the number of uniform divisions of the octave.
|
||||||
|
If unspecified, the default value of 12 is assumed (Western harmony).
|
||||||
"""
|
"""
|
||||||
self._tonality = tonality
|
self._tonality = tonality
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ class SetClass(list):
|
||||||
try:
|
try:
|
||||||
pitches.add(int(i) % tonality)
|
pitches.add(int(i) % tonality)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError("Requires integer arguments (use 10 and 11 for 'T' and 'E')")
|
raise TypeError("Requires integer arguments (use 10 and 11 for 'T' and 'E')")
|
||||||
|
|
||||||
# if necessary "rotate" so that the lowest non-zero interval is zero
|
# if necessary "rotate" so that the lowest non-zero interval is zero
|
||||||
if pitches:
|
if pitches:
|
||||||
|
|
@ -53,38 +53,60 @@ class SetClass(list):
|
||||||
super().append(i)
|
super().append(i)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Return the Python instance string representation."""
|
"""Return a string representation of this instance."""
|
||||||
s = f"SetClass{{{','.join(str(i) for i in self.pitch_classes)}}}"
|
s = f"SetClass{{{','.join(str(i) for i in self.pitch_classes)}}}"
|
||||||
return s if self.tonality == 12 else f"{s} (T={self.tonality})"
|
return s if self.tonality == 12 else f"{s} (T={self.tonality})"
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
|
"""Return a unique identifier for the instance, based on the :attr:`pitch_classes` used."""
|
||||||
return sum([hash(i) for i in self.pitch_classes])
|
return sum([hash(i) for i in self.pitch_classes])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pitch_classes(self) -> list:
|
def pitch_classes(self) -> list(int):
|
||||||
|
"""The pitch classes as an ordered :class:`list` of integers."""
|
||||||
return list(self)
|
return list(self)
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def tonality(self) -> int:
|
def tonality(self) -> int:
|
||||||
"""
|
"""
|
||||||
Returns the number of (equal) divisions of the octave. The default value is 12, which
|
The number of uniform divisions of the octave. The default value is 12, which
|
||||||
represents traditional Western chromatic harmony, the octave divided into twelve semitones.
|
represents traditional Western chromatic harmony, the octave divided into twelve semitones.
|
||||||
"""
|
"""
|
||||||
return self._tonality
|
return self._tonality
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def cardinality(self) -> int:
|
def cardinality(self) -> int:
|
||||||
"""Returns the cardinality of the set class, i.e. the number of pitch classes."""
|
"""
|
||||||
|
The cardinality of the set class, i.e. the number of :attr:`pitch_classes`.
|
||||||
|
"""
|
||||||
return len(self.pitch_classes)
|
return len(self.pitch_classes)
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def brightness(self) -> int:
|
def brightness(self) -> int:
|
||||||
"""Returns the brightness of the set class, defined as the sum of the pitch class values."""
|
"""
|
||||||
|
The brightness of the set class.
|
||||||
|
|
||||||
|
Brightness (B) is a property proposed by Brian Leonard, defined as the sum of the values of
|
||||||
|
the :attr:`pitch_classes` in the set class.
|
||||||
|
"""
|
||||||
return sum(self.pitch_classes)
|
return sum(self.pitch_classes)
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def adjacency_intervals(self) -> list:
|
def decimal(self) -> int:
|
||||||
"""Adjacency intervals between the pitch classes, used for Leonard notation subscripts."""
|
"""
|
||||||
|
The decimal value of the binary representation of the :attr:`pitch_classes`.
|
||||||
|
|
||||||
|
Returns the decimal value of the pitch classes expressed as a binary bit mask, i.e. the sum
|
||||||
|
of 2ⁱ where i is each pitch class value in ascending order. For example, set class {0,1,4,6}
|
||||||
|
has a binary value of 000001010011, which is the decimal value 83.
|
||||||
|
|
||||||
|
Further reading: Goyette (2012) p. 25, citing Brinkman (1986).
|
||||||
|
"""
|
||||||
|
return sum([2**i for i in self.pitch_classes])
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def adjacency_intervals(self) -> list(int):
|
||||||
|
"""The ordered :class:`list` of adjacency intervals between the pitch classes."""
|
||||||
if not self.pitch_classes:
|
if not self.pitch_classes:
|
||||||
return list()
|
return list()
|
||||||
intervals = list()
|
intervals = list()
|
||||||
|
|
@ -96,52 +118,85 @@ class SetClass(list):
|
||||||
intervals.append(self.tonality - prev)
|
intervals.append(self.tonality - prev)
|
||||||
return intervals
|
return intervals
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def z_relations(self) -> list:
|
|
||||||
"""
|
|
||||||
Return all distinct set classes with the same interval vector (Allen Forte: "Z-related").
|
|
||||||
For example, Forte 4-Z15 {0,1,4,6} and Forte 4-Z29 {0,1,3,7} both have iv⟨1,1,1,1,1,1⟩ but
|
|
||||||
are not inversions, complements, or transpositions (rotations) of each other.
|
|
||||||
"""
|
|
||||||
return [i for i in SetClass.darkest_of_cardinality(self.cardinality) if i.interval_vector == self.interval_vector]
|
|
||||||
|
|
||||||
@cache_property
|
|
||||||
def interval_vector(self) -> list:
|
def interval_vector(self) -> list:
|
||||||
"""
|
"""
|
||||||
An ordered tuple containing the multiplicities of each interval class in the set class.
|
An ordered :class:`list` containing the multiplicities of each interval class in the set
|
||||||
Denoted in angle-brackets, e.g.
|
class. Denoted in angle-brackets, e.g. the interval vector of {0,2,4,5,7,9,11} is
|
||||||
The interval vector of {0,2,4,5,7,9,11} is ⟨2,5,4,3,6,1⟩
|
⟨2,5,4,3,6,1⟩. Each element in the vector is the frequency of occurrence of the interval
|
||||||
— Rahn (1980), p. 100
|
represented by its ordinal position, i.e. ⟨2,5,4,3,6,1⟩ means two semitones, five major
|
||||||
|
seconds, four minor thirds, and so on. — Rahn (1980), p. 100.
|
||||||
"""
|
"""
|
||||||
from itertools import combinations
|
from itertools import combinations
|
||||||
iv = [0 for i in range(1, int(self.tonality / 2) + 1)]
|
iv = [0 for i in range(1, int(self.tonality / 2) + 1)]
|
||||||
for (a, b) in combinations(self.pitch_classes, 2):
|
for (a, b) in combinations(self.pitch_classes, 2):
|
||||||
ic = SetClass.unordered_interval(a, b)
|
ic = self.unordered_interval(a, b)
|
||||||
iv[ic - 1] += 1
|
iv[ic - 1] += 1
|
||||||
return iv
|
return iv
|
||||||
|
|
||||||
def ordered_interval(a: int, b: int) -> int:
|
@cached_property
|
||||||
|
def z_relations(self) -> list:
|
||||||
"""
|
"""
|
||||||
The ordered interval or "directed interval" (Babbitt) of two pitch classes is determined by
|
A :class:`list` of the Z-relations of this set class.
|
||||||
the difference of the pitch class values, modulo 12:
|
|
||||||
i⟨a,b⟩ = b-a mod 12 — Rahn (1980), p. 25
|
|
||||||
"""
|
|
||||||
return (b % 12 - a % 12) % 12
|
|
||||||
|
|
||||||
def unordered_interval(a: int, b: int) -> int:
|
Allen Forte in his book *The Structure of Atonal Music* (1973) described the relationship
|
||||||
|
between twins of set classes that share the same :attr:`interval_vector`, but are not
|
||||||
|
related by :attr:`inversion`, :attr:`complement`, or transposition, as Z-related ('Z' for
|
||||||
|
*zygote* from Greek: ζυγωτός, 'joined' or 'paired'). This property returns all distinct set
|
||||||
|
classes with the same interval vector.
|
||||||
|
|
||||||
|
For example, Forte 4-Z15 {0,1,4,6} and Forte 4-Z29 {0,1,3,7} both have iv⟨1,1,1,1,1,1⟩ but
|
||||||
|
are not inversions, complements, or transpositions of each other.
|
||||||
"""
|
"""
|
||||||
|
return [i for i in SetClass.darkest_of_cardinality(self.cardinality) if i.interval_vector == self.interval_vector]
|
||||||
|
|
||||||
|
def ordered_interval(self, a: int, b: int) -> int:
|
||||||
|
"""
|
||||||
|
Return the ordered interval of two pitch classes.
|
||||||
|
|
||||||
|
The ordered interval, or "directed interval" (Babbitt) of two :attr:`pitch_classes` is
|
||||||
|
determined by the difference of the pitch class values, modulo :attr:`tonality`. For example
|
||||||
|
with tonality 12:
|
||||||
|
|
||||||
|
i⟨a,b⟩ = b-a mod 12
|
||||||
|
|
||||||
|
— Rahn (1980), p. 25
|
||||||
|
|
||||||
|
Args:
|
||||||
|
a (int): the first pitch class value.
|
||||||
|
b (int): the second pitch class value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The ordered interval as ann integer.
|
||||||
|
"""
|
||||||
|
return (b % self.tonality - a % self.tonality) % self.tonality
|
||||||
|
|
||||||
|
def unordered_interval(self, a: int, b: int) -> int:
|
||||||
|
"""
|
||||||
|
Return the unordered interval of two pitch classes.
|
||||||
|
|
||||||
The unordered interval (also "interval distance", "interval class", "ic", or "undirected
|
The unordered interval (also "interval distance", "interval class", "ic", or "undirected
|
||||||
interval") of two pitch classes is the smaller of the two possible ordered intervals
|
interval") of two :attr:`pitch_classes` is the smaller of the possible values of
|
||||||
(differences in pitch class value):
|
:attr:`ordered_interval` (differences in pitch class value):
|
||||||
i(a,b) = min: i⟨a,b⟩, i⟨b,a⟩ — Rahn (1980), p. 28
|
|
||||||
"""
|
|
||||||
return min(SetClass.ordered_interval(a, b), SetClass.ordered_interval(b, a))
|
|
||||||
|
|
||||||
@cache_property
|
i(a,b) = min: i⟨a,b⟩, i⟨b,a⟩
|
||||||
def versions(self) -> list:
|
|
||||||
|
— Rahn (1980), p. 28
|
||||||
|
|
||||||
|
Args:
|
||||||
|
a (int): the first pitch class value.
|
||||||
|
b (int): the second pitch class value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The ordered interval as ann integer.
|
||||||
"""
|
"""
|
||||||
Returns all possible zero-normalised versions (clock rotations) of this set class,
|
return min(self.ordered_interval(a, b), self.ordered_interval(b, a))
|
||||||
sorted by brightness. See Rahn (1980) Set types, Tₙ
|
|
||||||
|
@cached_property
|
||||||
|
def versions(self) -> list(SetClass):
|
||||||
|
"""
|
||||||
|
All possible zero-normalised transpositions of this set class, sorted by :attr:`brightness`.
|
||||||
|
See Rahn (1980), Tₙ set types.
|
||||||
"""
|
"""
|
||||||
# The empty set class has one version, itself
|
# The empty set class has one version, itself
|
||||||
if not self.pitch_classes:
|
if not self.pitch_classes:
|
||||||
|
|
@ -155,12 +210,16 @@ class SetClass(list):
|
||||||
versions.sort(key=lambda x: x.brightness)
|
versions.sort(key=lambda x: x.brightness)
|
||||||
return versions
|
return versions
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def rahn_normal_form(self) -> SetClass:
|
def rahn_normal_form(self) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Return the Rahn normal form of the set class; Leonard describes this as "most dispersed from
|
The Rahn normal form of the set class.
|
||||||
the right". Find the smallest outside interval, and proceed inwards from the right until one
|
|
||||||
result remains. See Rahn (1980), p. 33
|
John Rahn's normal form described in his book *Basic Atonal Theory* (1980) is an algorithm
|
||||||
|
to produce a unique form for each set class. Often wrongly described as "most packed to the
|
||||||
|
left", Leonard describes it as "most dispersed from the right". Find the smallest outside
|
||||||
|
interval, and if necessary proceed inwards from the right finding the smallest next interval
|
||||||
|
until one result remains. See Rahn (1980), p. 33.
|
||||||
"""
|
"""
|
||||||
def _most_dispersed(versions, n):
|
def _most_dispersed(versions, n):
|
||||||
return [i for i in versions if i.pitch_classes[-n] == min([i.pitch_classes[-n] for i in versions])]
|
return [i for i in versions if i.pitch_classes[-n] == min([i.pitch_classes[-n] for i in versions])]
|
||||||
|
|
@ -172,11 +231,20 @@ class SetClass(list):
|
||||||
n += 1
|
n += 1
|
||||||
return versions[0]
|
return versions[0]
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
|
def rahn_prime_form(self) -> SetClass:
|
||||||
|
"""
|
||||||
|
The Rahn prime is the most dispersed from the right of the Rahn normal forms of a set class
|
||||||
|
and its inversion.
|
||||||
|
"""
|
||||||
|
prime = min(self.rahn_normal_form.decimal, self.inversion.rahn_normal_form.decimal)
|
||||||
|
return self.inversion.rahn_normal_form if self.inversion.rahn_normal_form.decimal == prime else self.inversion.rahn_normal_form
|
||||||
|
|
||||||
|
@cached_property
|
||||||
def packed_left(self) -> SetClass:
|
def packed_left(self) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Return the form of the set class that is most packed to the left (smallest adjacency
|
The form of the set class that is most packed to the left (the smallest adjacency intervals
|
||||||
intervals to the left). Find the smallest adjacency interval, and proceed towards the right
|
to the left). Find the smallest first adjacency interval, and proceed towards the right
|
||||||
until one result remains.
|
until one result remains.
|
||||||
"""
|
"""
|
||||||
def _most_packed(versions, n):
|
def _most_packed(versions, n):
|
||||||
|
|
@ -189,14 +257,15 @@ class SetClass(list):
|
||||||
n += 1
|
n += 1
|
||||||
return versions[0]
|
return versions[0]
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def prime_form(self) -> SetClass:
|
def prime_form(self) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Return the prime form of the set class. Find the forms with the smallest outside interval,
|
Return the prime form of the set class.
|
||||||
and if necessary chose the form most packed to the left (the smallest adjacency intervals
|
|
||||||
working from left to right).
|
Allen Forte describes the algorithm for finding this normal form in his book *The Structure
|
||||||
Allen Forte describes the algorithm in his book *The Structure of Atonal Music* (1973) in
|
of Atonal Music* (1973) in section 1.2 (pp. 3-5), citing Milton Babbitt (1961). Find the
|
||||||
section 1.2 (pp. 3-5), citing Milton Babbitt (1961).
|
forms with the smallest outside interval, and if necessary chose the form most packed to the
|
||||||
|
left (the smallest adjacency intervals working from left to right).
|
||||||
"""
|
"""
|
||||||
def _most_packed(versions, n):
|
def _most_packed(versions, n):
|
||||||
return [i for i in versions if i.pitch_classes[n] == min([i.pitch_classes[n] for i in versions])]
|
return [i for i in versions if i.pitch_classes[n] == min([i.pitch_classes[n] for i in versions])]
|
||||||
|
|
@ -212,14 +281,36 @@ class SetClass(list):
|
||||||
n += 1
|
n += 1
|
||||||
return versions[0]
|
return versions[0]
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
|
def forte_prime(self) -> SetClass:
|
||||||
|
"""
|
||||||
|
Return the Forte prime of the set class, including its inversions. Forte's list of named set
|
||||||
|
classes included inversions, for instance his "3-11" set class has a normal form of {0,3,7}
|
||||||
|
which describes the minor chord, and also describes its (set) inversion {0,5,9} which has a
|
||||||
|
:attr:`prime_form` of {0,4,7}, the major chord.
|
||||||
|
"""
|
||||||
|
return min(self, self.inversion)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def forte_name(self) -> str:
|
||||||
|
"""
|
||||||
|
Return the Forte name of this set class. Only applies to 12-tonality set classes.
|
||||||
|
|
||||||
|
Allen Forte listed and named all possible prime set classes in his book *The Structure
|
||||||
|
of Atonal Music* (1973) in Appendix 1, p. 179-181.
|
||||||
|
"""
|
||||||
|
if self.tonality != 12:
|
||||||
|
return ''
|
||||||
|
from setclass.forte import FORTE_PRIMES
|
||||||
|
d = self.forte_prime.decimal
|
||||||
|
return FORTE_PRIMES[d] if d in FORTE_PRIMES else ''
|
||||||
|
|
||||||
|
@cached_property
|
||||||
def darkest_form(self) -> SetClass:
|
def darkest_form(self) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Returns the version with the smallest brightness value, most packed to the left.
|
The version of this set class with the smallest :attr:`brightness` value, most packed to the
|
||||||
|
left.
|
||||||
"""
|
"""
|
||||||
if not self.versions:
|
|
||||||
return self
|
|
||||||
|
|
||||||
B = min(i.brightness for i in self.versions)
|
B = min(i.brightness for i in self.versions)
|
||||||
versions = [i for i in self.versions if i.brightness == B]
|
versions = [i for i in self.versions if i.brightness == B]
|
||||||
if len(versions) == 1:
|
if len(versions) == 1:
|
||||||
|
|
@ -234,61 +325,91 @@ class SetClass(list):
|
||||||
n += 1
|
n += 1
|
||||||
return versions[0]
|
return versions[0]
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def brightest_form(self) -> SetClass:
|
def brightest_form(self) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Returns the version with the largest brightness value.
|
The version of this set class with the largest :attr:`brightness` value, most packed to the
|
||||||
TODO: How to break a tie? Or return all matches in a tuple? Sorted by what?
|
right.
|
||||||
"""
|
"""
|
||||||
return self.versions[-1] if self.versions else self
|
B = max(i.brightness for i in self.versions)
|
||||||
|
versions = [i for i in self.versions if i.brightness == B]
|
||||||
|
if len(versions) == 1:
|
||||||
|
return versions[0]
|
||||||
|
|
||||||
@cache_property
|
def _most_packed(versions, n):
|
||||||
|
return [i for i in versions if i.pitch_classes[n] == min([i.pitch_classes[n] for i in versions])]
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
while len(versions) > 1:
|
||||||
|
versions = _most_packed(versions, n)
|
||||||
|
n += 1
|
||||||
|
return versions[0]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
def inversion(self) -> SetClass:
|
def inversion(self) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Returns the inversion of this set class, equivalent of reflection through the 0 axis on a
|
The inversion of this set class, transposed so the smallest pitch class is 0. Equivalent to
|
||||||
clock diagram.
|
a reflection through the 0 axis on a clock diagram.
|
||||||
"""
|
"""
|
||||||
return SetClass(*[self.tonality - i for i in self.pitch_classes], tonality=self.tonality)
|
return SetClass(*[self.tonality - i for i in self.pitch_classes], tonality=self.tonality)
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def is_symmetrical(self) -> bool:
|
def is_symmetrical(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns whether this set class is symmetrical upon inversion, for example Forte 5-Z17:
|
Whether this set class is symmetrical upon inversion, for example Forte 5-Z37:
|
||||||
{0,1,2,5,9} → {0,4,8,9,11}
|
|
||||||
|
>>> sc = SetClass(0, 1, 2, 5, 9)
|
||||||
|
>>> sc.rahn_normal_form
|
||||||
|
SetClass{0,3,4,5,8}
|
||||||
|
>>> sc.inversion.rahn_normal_form
|
||||||
|
SetClass{0,3,4,5,8}
|
||||||
|
>>> sc.is_symmetrical
|
||||||
|
True
|
||||||
"""
|
"""
|
||||||
return self.darkest_form == self.inversion.darkest_form
|
return self.darkest_form == self.inversion.darkest_form
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def complement(self) -> SetClass:
|
def complement(self) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Returns the set class containing all pitch classes absent in this one (rotated so the
|
The set class containing all :attr:`pitch_classes` absent in this one, transposed so the
|
||||||
smallest is 0).
|
smallest pitch class is 0.
|
||||||
"""
|
"""
|
||||||
return SetClass(*[i for i in range(self.tonality) if i not in self.pitch_classes], tonality=self.tonality)
|
return SetClass(*[i for i in range(self.tonality) if i not in self.pitch_classes], tonality=self.tonality)
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def dozenal_notation(self) -> str:
|
def dozenal_notation(self) -> str:
|
||||||
"""
|
"""
|
||||||
If tonality is no greater than 12, return a string representation using Dozenal Society
|
A string representation using ↊ and ↋ for 10 and 11.
|
||||||
characters '↊' for 10 and '↋' for 11 (the Pitman forms from Unicode 8.0 release, 2015).
|
|
||||||
|
For a set class with a :attr:`tonality` no greater than 12, this property replaces the 10
|
||||||
|
and 11 pitch classes with the Dozenal Society characters '↊' and '↋' respectively. These are
|
||||||
|
the Pitman forms from the Unicode 8.0 specification released in 2015.
|
||||||
"""
|
"""
|
||||||
return f"{self}" if self.tonality > 12 else f"{self}".replace('10', '↊').replace('11', '↋')
|
return f"{self}" if self.tonality > 12 else f"{self}".replace('10', '↊').replace('11', '↋')
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def duodecimal_notation(self) -> str:
|
def duodecimal_notation(self) -> str:
|
||||||
"""
|
"""
|
||||||
If tonality is no greater than 12, replace 10 and 11 with 'T' and 'E'.
|
A string representation using T and E for 10 and 11.
|
||||||
|
|
||||||
|
For a set class with a :attr:`tonality` no greater than 12, this property replaces the 10
|
||||||
|
and 11 pitch classes with the letters 'T' and 'E' respectively.
|
||||||
"""
|
"""
|
||||||
return f"{self}" if self.tonality > 12 else f"{self}".replace('10', 'T').replace('11', 'E')
|
return f"{self}" if self.tonality > 12 else f"{self}".replace('10', 'T').replace('11', 'E')
|
||||||
|
|
||||||
@cache_property
|
@cached_property
|
||||||
def leonard_notation(self) -> str:
|
def leonard_notation(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a string representation of this set class using subscripts to denote the adjacency
|
The string representation proposed by Brian Leonard.
|
||||||
intervals between the pitch classes instead of commas, and the overall brightness (sum of
|
|
||||||
pitch class values) denoted as a superscript. In standard tonality (T=12) the letters T and
|
Returns a string representation of this set class using subscripts to denote the
|
||||||
E are used for pitch classes 10 and 11. For example, Forte 7-34, {0,1,3,4,6,8,10} is
|
:attr:`adjacency_intervals` between the pitch classes instead of commas, and the overall
|
||||||
notated [0₁1₂3₁4₂6₂8₂T₂]⁽³²⁾.
|
:attr:`brightness` (sum of the values of :attr:`pitch_classes`) denoted as a superscript.
|
||||||
|
In standard tonality (T=12) the letters T and E are used for pitch classes 10 and 11. For
|
||||||
|
example, Forte 7-34 would be written as [0₁1₂3₁4₂6₂8₂T₂]⁽³²⁾ in this scheme:
|
||||||
|
|
||||||
|
>>> SetClass.from_string('{0,1,3,4,6,8,10}').leonard_notation
|
||||||
|
[0₁1₂3₁4₂6₂8₂T₂]⁽³²⁾
|
||||||
"""
|
"""
|
||||||
# Return numbers as subscript and superscript strings:
|
# Return numbers as subscript and superscript strings:
|
||||||
def subscript(n: int) -> str:
|
def subscript(n: int) -> str:
|
||||||
|
|
@ -309,19 +430,107 @@ class SetClass(list):
|
||||||
# Class methods -----------------------------------------------------------
|
# Class methods -----------------------------------------------------------
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_string(this, string: str) -> SetClass:
|
def from_string(this, string: str, tonality: int = 12) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Attempt to create a SetClass from any string containing a sequence of zero or more integers.
|
Create a :class:`SetClass` from a string.
|
||||||
A useful convenience function, e.g. SetClass.from_string('{0,3,7,9}')
|
|
||||||
|
A useful convenience function for converting from various text representations of set
|
||||||
|
classes that contain a sequence of zero or more integers. Non-integer content is ignored,
|
||||||
|
and integers must be separated by some non-integer character(s), usually a space, comma, or
|
||||||
|
similar. For instance:
|
||||||
|
|
||||||
|
>>> SetClass.from_string("Prélude à l'après-midi d'un faune uses [0,2,4,6,8,9]")
|
||||||
|
SetClass{0,2,4,6,8,9}
|
||||||
|
>>> SetClass.from_string('{0, 3, 7}', tonality=8)
|
||||||
|
SetClass{0,3,7} (T=8)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
string (str):
|
||||||
|
Any string representation of a set class, containing some integer content.
|
||||||
|
tonality (int):
|
||||||
|
The :attr:`tonality`, or modulus, is the number of uniform divisions of the octave.
|
||||||
|
If unspecified, the default value of 12 is assumed (Western harmony).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A :class:`SetClass` instance with the pitch classes found in the string.
|
||||||
"""
|
"""
|
||||||
return SetClass(*re.findall(r'\d+', string))
|
return SetClass(*re.findall(r'\d+', string), tonality=tonality)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all_of_cardinality(cls, cardinality: int, tonality: int = 12) -> set:
|
def from_decimal(this, decimal: int, tonality: int = 12) -> SetClass:
|
||||||
"""
|
"""
|
||||||
Returns all set classes of a given cardinality. Tonality can be specified, default is 12.
|
Create a :class:`SetClass` from its decimal number.
|
||||||
Warning: high values of tonality can take a long time to calculate (T=24 takes about a
|
|
||||||
|
A useful convenience function for converting from a decimal representation of set classes.
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
>>> SetClass.from_decimal(145)
|
||||||
|
SetClass{0,4,7}
|
||||||
|
>>> SetClass.from_decimal(137, tonality=8)
|
||||||
|
SetClass{0,3,7} (T=8)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
decmial (int):
|
||||||
|
The decimal to convert from.
|
||||||
|
tonality (int):
|
||||||
|
The :attr:`tonality`, or modulus, is the number of uniform divisions of the octave.
|
||||||
|
If unspecified, the default value of 12 is assumed (Western harmony).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A :class:`SetClass` instance with the pitch classes encoded in the decimal representation.
|
||||||
|
"""
|
||||||
|
if decimal.bit_length() > tonality:
|
||||||
|
raise ValueError("Decimal number too large for tonality's maximum cardinality")
|
||||||
|
pc = list()
|
||||||
|
for i in range(decimal.bit_length()):
|
||||||
|
if decimal & (2 ** i):
|
||||||
|
pc.append(i)
|
||||||
|
return SetClass(*pc, tonality=tonality)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_forte_name(this, name: str) -> SetClass:
|
||||||
|
"""
|
||||||
|
Create a :class:`SetClass` from its Forte prime form designation. Assumes 12-tonality.
|
||||||
|
Name is case insensitive.
|
||||||
|
|
||||||
|
A useful convenience function for creating a set class from its Forte name. For instance:
|
||||||
|
|
||||||
|
>>> SetClass.from_forte_name('3-11')
|
||||||
|
SetClass{0,3,7}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str):
|
||||||
|
The Forte name (case insensitive).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A :class:`SetClass` instance in Forte prime form.
|
||||||
|
"""
|
||||||
|
from setclass.forte import FORTE_PRIMES
|
||||||
|
decimal = next((k for k, v in FORTE_PRIMES.items() if v == str(name).upper()), None)
|
||||||
|
if decimal:
|
||||||
|
return SetClass.from_decimal(decimal)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Forte name '{name}' not found.")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all_of_cardinality(this, cardinality: int, tonality: int = 12) -> set:
|
||||||
|
"""
|
||||||
|
Returns a :class:`set` of all possible :class:`SetClass` objects with a given
|
||||||
|
:attr:`cardinality`.
|
||||||
|
|
||||||
|
.. warning:: High values of tonality can take a long time to calculate (T=24 takes about a
|
||||||
minute on an Intel i7-13700H CPU).
|
minute on an Intel i7-13700H CPU).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cardinality (int):
|
||||||
|
The :attr:`cardinality` of the set classes to return.
|
||||||
|
tonality (int):
|
||||||
|
The :attr:`tonality`, or modulus, is the number of uniform divisions of the octave.
|
||||||
|
If unspecified, the default value of 12 is assumed (Western harmony).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A :class:`set` of :class:`SetClass` objects.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def _powerset(seq):
|
def _powerset(seq):
|
||||||
"""Set of all possible unique subsets."""
|
"""Set of all possible unique subsets."""
|
||||||
|
|
@ -334,36 +543,72 @@ class SetClass(list):
|
||||||
return set(SetClass(*i, tonality=tonality) for i in _powerset(range(tonality)) if len(i) == cardinality and 0 in i)
|
return set(SetClass(*i, tonality=tonality) for i in _powerset(range(tonality)) if len(i) == cardinality and 0 in i)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def darkest_of_cardinality(this, cardinality: int, tonality: int = 12, prime: bool = False) -> set:
|
def darkest_of_cardinality(this, cardinality: int, tonality: int = 12) -> set:
|
||||||
"""
|
"""
|
||||||
Returns all set classes of a given cardinality, in their darkest forms (ignore rotations).
|
Returns a :class:`set` of all :class:`SetClass` objects with a given :attr:`cardinality` in
|
||||||
Tonality can be specified, default is 12.
|
their darkest forms.
|
||||||
Warning: high values of tonality can take a long time to calculate (T=24 takes about a
|
|
||||||
|
This produces a smaller set than :attr:`all_of_cardinality`, since it eliminates set classes
|
||||||
|
that are "brighter" transpositions of the darkest :class:`SetClass` returned by the
|
||||||
|
:attr:`darkest_form` property.
|
||||||
|
|
||||||
|
.. warning:: High values of tonality can take a long time to calculate (T=24 takes about a
|
||||||
minute on an Intel i7-13700H CPU).
|
minute on an Intel i7-13700H CPU).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cardinality (int):
|
||||||
|
The :attr:`cardinality` of the set classes to return.
|
||||||
|
tonality (int):
|
||||||
|
The :attr:`tonality`, or modulus, is the number of uniform divisions of the octave.
|
||||||
|
If unspecified, the default value of 12 is assumed (Western harmony).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A :class:`set` of :class:`SetClass` objects in :attr:`darkest_form`.
|
||||||
"""
|
"""
|
||||||
return set(i.darkest_form for i in this.all_of_cardinality(cardinality, tonality))
|
return set(i.darkest_form for i in this.all_of_cardinality(cardinality, tonality))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def normal_of_cardinality(this, cardinality: int, tonality: int = 12, prime: bool = False) -> set:
|
def normal_of_cardinality(this, cardinality: int, tonality: int = 12) -> set:
|
||||||
"""
|
"""
|
||||||
Returns all set classes of a given cardinality, in their (darkest) Rahn normal forms (ignore
|
Returns a :class:`set` of all :class:`SetClass` objects with a given :attr:`cardinality` in
|
||||||
rotations). Tonality can be specified, default is 12.
|
their Rahn normal form.
|
||||||
Warning: high values of tonality can take a long time to calculate (T=24 takes about a
|
|
||||||
|
This produces a smaller set than :attr:`all_of_cardinality`, since it eliminates set classes
|
||||||
|
that are transpositions of the normalised :class:`SetClass` returned by the
|
||||||
|
:attr:`rahn_normal_form` property.
|
||||||
|
|
||||||
|
.. warning:: High values of tonality can take a long time to calculate (T=24 takes about a
|
||||||
minute on an Intel i7-13700H CPU).
|
minute on an Intel i7-13700H CPU).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cardinality (int):
|
||||||
|
The :attr:`cardinality` of the set classes to return.
|
||||||
|
tonality (int):
|
||||||
|
The :attr:`tonality`, or modulus, is the number of uniform divisions of the octave.
|
||||||
|
If unspecified, the default value of 12 is assumed (Western harmony).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A :class:`set` of :class:`SetClass` objects in :attr:`rahn_normal_form`.
|
||||||
"""
|
"""
|
||||||
return set(i.rahn_normal_form for i in this.all_of_cardinality(cardinality, tonality))
|
return set(i.rahn_normal_form for i in this.all_of_cardinality(cardinality, tonality))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def bright_rahn_normal_forms(this) -> set:
|
def bright_rahn_normal_forms(this) -> set:
|
||||||
"""
|
"""
|
||||||
|
The set of Rahn normal forms that are not the same as the darkest form.
|
||||||
|
|
||||||
John Rahn's normal form from *Basic Atonal Theory* (1980) is an algorithm to produce a
|
John Rahn's normal form from *Basic Atonal Theory* (1980) is an algorithm to produce a
|
||||||
unique form for each set class. Most of the time it is also the "darkest" (smallest
|
unique form for each set class (see :attr:`rahn_normal_form`). Most of the time it is also
|
||||||
brightness value), except for all the times when that is not the case; this function returns
|
the same as the :attr:`darkest_form` (that with the smallest :attr:`brightness` value),
|
||||||
that list, as a set of (cardinality, darkest, rahn) tuples.
|
except for all the times when that is not the case!
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A :class:`set` of (:attr:`cardinality`, :attr:`darkest_form`, :attr:`rahn_normal_form`)
|
||||||
|
tuples.
|
||||||
"""
|
"""
|
||||||
cases = set()
|
cases = set()
|
||||||
for C in range(13): # 0 to 12
|
for C in range(13): # 0 to 12
|
||||||
for sc in SetClass.all_of_cardinality(C):
|
for sc in this.all_of_cardinality(C):
|
||||||
if sc.darkest_form.brightness < sc.rahn_normal_form.brightness:
|
if sc.darkest_form.brightness < sc.rahn_normal_form.brightness:
|
||||||
cases.add((C, sc.darkest_form, sc.rahn_normal_form))
|
cases.add((C, sc.darkest_form, sc.rahn_normal_form))
|
||||||
return cases
|
return cases
|
||||||
|
|
@ -371,15 +616,21 @@ class SetClass(list):
|
||||||
@classmethod
|
@classmethod
|
||||||
def bright_prime_forms(this) -> set:
|
def bright_prime_forms(this) -> set:
|
||||||
"""
|
"""
|
||||||
|
The set of Forte prime forms that are not the same as the darkest form.
|
||||||
|
|
||||||
Allen Forte describes the algorithm for deriving the "prime" or zeroed normal form, in his
|
Allen Forte describes the algorithm for deriving the "prime" or zeroed normal form, in his
|
||||||
book *The Structure of Atonal Music* (1973) in section 1.2 (pp. 3-5), citing Milton Babbitt
|
book *The Structure of Atonal Music* (1973) in section 1.2 (pp. 3-5), citing Milton Babbitt
|
||||||
(1961). It produces a unique form for each set class, which most of the time is also the
|
(1961). It produces a unique form for each set class (see :attr:`prime_form`), which most of
|
||||||
"darkest" (smallest brightness value), except for all the times when that is not the case;
|
the time is the same as the :attr:`darkest_form` (that with the smallest :attr:`brightness`
|
||||||
this function returns that list, as a set of (cardinality, darkest, prime) tuples.
|
value), except for all the times when that is not the case!
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A :class:`set` of (:attr:`cardinality`, :attr:`darkest_form`, :attr:`prime_form`) tuples.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cases = set()
|
cases = set()
|
||||||
for C in range(13): # 0 to 12
|
for C in range(13): # 0 to 12
|
||||||
for sc in SetClass.all_of_cardinality(C):
|
for sc in this.all_of_cardinality(C):
|
||||||
if sc.darkest_form.brightness < sc.prime_form.brightness:
|
if sc.darkest_form.brightness < sc.prime_form.brightness:
|
||||||
cases.add((C, sc.darkest_form, sc.prime_form))
|
cases.add((C, sc.darkest_form, sc.prime_form))
|
||||||
return cases
|
return cases
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import pytest
|
||||||
from setclass.setclass import SetClass
|
from setclass.setclass import SetClass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -5,6 +6,16 @@ from setclass.setclass import SetClass
|
||||||
# Functional tests
|
# Functional tests
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_init_args():
|
||||||
|
empty = SetClass()
|
||||||
|
assert empty.pitch_classes == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_integer_init_args():
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
SetClass('Z', 'abc')
|
||||||
|
|
||||||
|
|
||||||
def test_zero_normalised_pc():
|
def test_zero_normalised_pc():
|
||||||
"Pitch classes are normalised to start at zero"
|
"Pitch classes are normalised to start at zero"
|
||||||
a = SetClass(0, 1, 3)
|
a = SetClass(0, 1, 3)
|
||||||
|
|
@ -66,6 +77,99 @@ def test_interval_vector_invarant():
|
||||||
assert version.interval_vector == iv
|
assert version.interval_vector == iv
|
||||||
|
|
||||||
|
|
||||||
|
def test_z_relations():
|
||||||
|
# Forte 4-Z15 {0,1,4,6} and Forte 4-Z29 {0,1,3,7} both have iv⟨1,1,1,1,1,1⟩
|
||||||
|
z1 = SetClass.from_forte_name('4-Z15')
|
||||||
|
z2 = SetClass.from_forte_name('4-Z29')
|
||||||
|
assert z1 == SetClass(0, 1, 4, 6)
|
||||||
|
assert z2 == SetClass(0, 1, 3, 7)
|
||||||
|
assert z1 in z1.z_relations
|
||||||
|
assert z1 in z2.z_relations
|
||||||
|
assert z2 in z1.z_relations
|
||||||
|
assert z2 in z2.z_relations
|
||||||
|
|
||||||
|
|
||||||
|
def test_forte_name():
|
||||||
|
tests = [
|
||||||
|
('1-1', [0]),
|
||||||
|
('3-11', [0, 3, 7]),
|
||||||
|
('6-34', [0, 1, 3, 5, 7, 9]),
|
||||||
|
('6-35', [0, 2, 4, 6, 8, 10]),
|
||||||
|
('6-Z43', [0, 1, 2, 5, 6, 8]),
|
||||||
|
('12-1', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
|
||||||
|
]
|
||||||
|
for (name, pitch_classes) in tests:
|
||||||
|
assert SetClass(*pitch_classes).forte_name == name
|
||||||
|
|
||||||
|
|
||||||
|
def test_forte_name_tonality():
|
||||||
|
assert SetClass(0, 1, 2, tonality=13).forte_name == ''
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Class methods
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_string():
|
||||||
|
tests = [
|
||||||
|
("No integer values in this string", []),
|
||||||
|
("Here is a 1", [0]),
|
||||||
|
("{0, 3, 7}", [0, 3, 7]),
|
||||||
|
(" 0 4 7 ", [0, 4, 7]),
|
||||||
|
("Prélude à l'après-midi d'un faune uses [0,2,4,6,8,9]", [0, 2, 4, 6, 8, 9]),
|
||||||
|
("Works from Leonard notation: [0₁1₂3₁4₂6₂8₂9₃]⁽³¹⁾", [0, 1, 3, 4, 6, 8, 9]),
|
||||||
|
("Repeated numbers: 0,0,1,1,6,8", [0, 1, 6, 8]),
|
||||||
|
("Modulo numbers: 1, 22, 3, 2, 12, 144", [0, 1, 2, 3, 10]),
|
||||||
|
]
|
||||||
|
for (string, pitch_classes) in tests:
|
||||||
|
assert SetClass.from_string(string).pitch_classes == pitch_classes
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_decimal():
|
||||||
|
tests = [
|
||||||
|
(0, []),
|
||||||
|
(1, [0]),
|
||||||
|
(137, [0, 3, 7]),
|
||||||
|
(145, [0, 4, 7]),
|
||||||
|
(853, [0, 2, 4, 6, 8, 9]),
|
||||||
|
(1365, [0, 2, 4, 6, 8, 10]),
|
||||||
|
(4095, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
|
||||||
|
]
|
||||||
|
for (decimal, pitch_classes) in tests:
|
||||||
|
assert SetClass.from_decimal(decimal).pitch_classes == pitch_classes
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_decimal_error():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
SetClass.from_decimal(4096)
|
||||||
|
# with pytest.raises(ValueError):
|
||||||
|
# sc = SetClass.from_decimal(2048, tonality=11)
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_forte_name():
|
||||||
|
tests = [
|
||||||
|
('1-1', [0]),
|
||||||
|
('3-11', [0, 3, 7]),
|
||||||
|
('6-34', [0, 1, 3, 5, 7, 9]),
|
||||||
|
('6-35', [0, 2, 4, 6, 8, 10]),
|
||||||
|
('6-Z43', [0, 1, 2, 5, 6, 8]),
|
||||||
|
('12-1', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
|
||||||
|
]
|
||||||
|
for (name, pitch_classes) in tests:
|
||||||
|
assert SetClass.from_forte_name(name).pitch_classes == pitch_classes
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_forte_name_errors():
|
||||||
|
tests = [
|
||||||
|
'0-1',
|
||||||
|
'',
|
||||||
|
'Wibble',
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
for name in tests:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
SetClass.from_forte_name(name)
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Example Forte 5-20 set class
|
# Example Forte 5-20 set class
|
||||||
f520 = SetClass(0, 1, 5, 6, 8)
|
f520 = SetClass(0, 1, 5, 6, 8)
|
||||||
|
|
@ -86,6 +190,11 @@ def test_brightness():
|
||||||
assert f520.brightness == 20
|
assert f520.brightness == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_decimal():
|
||||||
|
"Set classes have a unique decimal number (sum of 2 raised to each normalised pitch class value)"
|
||||||
|
assert f520.decimal == 355
|
||||||
|
|
||||||
|
|
||||||
def test_pitch_classes():
|
def test_pitch_classes():
|
||||||
"Set classes have pitch classes"
|
"Set classes have pitch classes"
|
||||||
assert len(f520) == 5
|
assert len(f520) == 5
|
||||||
|
|
@ -102,11 +211,21 @@ def test_versions():
|
||||||
assert len(f520.versions) == 5
|
assert len(f520.versions) == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_symmetrical():
|
||||||
|
sc = SetClass(0, 1, 2, 5, 9)
|
||||||
|
assert sc.rahn_normal_form == sc.inversion.rahn_normal_form
|
||||||
|
assert sc.is_symmetrical
|
||||||
|
|
||||||
|
sc = SetClass(0, 1, 2, 5, 8)
|
||||||
|
assert sc.rahn_normal_form != sc.inversion.rahn_normal_form
|
||||||
|
assert not sc.is_symmetrical
|
||||||
|
|
||||||
|
|
||||||
def test_brightest_form():
|
def test_brightest_form():
|
||||||
b = f520.brightest_form
|
b = SetClass(0, 1, 2, 3, 4, 6, 7, 8, 10)
|
||||||
assert b in f520.versions
|
assert b.brightest_form in b.versions
|
||||||
assert b == SetClass(0, 4, 5, 9, 10)
|
assert b.brightest_form == SetClass(0, 2, 3, 4, 6, 8, 9, 10, 11)
|
||||||
assert b.leonard_notation == '[0₄4₁5₄9₁T₂]⁽²⁸⁾'
|
assert b.leonard_notation == '[0₁1₁2₁3₁4₂6₁7₁8₂T₂]⁽⁴¹⁾'
|
||||||
|
|
||||||
|
|
||||||
def test_darkest_form():
|
def test_darkest_form():
|
||||||
|
|
@ -122,6 +241,21 @@ def test_rahn_normal_form():
|
||||||
assert r == SetClass(0, 1, 5, 6, 8)
|
assert r == SetClass(0, 1, 5, 6, 8)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rahn_prime_form():
|
||||||
|
r = f520.rahn_prime_form
|
||||||
|
assert r == SetClass(0, 2, 3, 7, 8)
|
||||||
|
assert r not in f520.versions
|
||||||
|
assert r in f520.inversion.versions
|
||||||
|
# prime = min(self.rahn_normal_form.decimal, self.inversion.rahn_normal_form.decimal)
|
||||||
|
# return self.inversion.rahn_normal_form if self.inversion.rahn_normal_form.decimal == prime else self.inversion.rahn_normal_form
|
||||||
|
|
||||||
|
|
||||||
|
def test_packed_left():
|
||||||
|
r = f520.packed_left
|
||||||
|
assert r == SetClass(0, 1, 3, 7, 8)
|
||||||
|
assert r in f520.versions
|
||||||
|
|
||||||
|
|
||||||
def test_inversion():
|
def test_inversion():
|
||||||
i = f520.inversion
|
i = f520.inversion
|
||||||
assert i not in f520.versions
|
assert i not in f520.versions
|
||||||
|
|
@ -153,6 +287,10 @@ def test_complement_brightness():
|
||||||
assert f520.complement.brightness == 32
|
assert f520.complement.brightness == 32
|
||||||
|
|
||||||
|
|
||||||
|
def test_complement_decimal():
|
||||||
|
assert f520.complement.decimal == 935
|
||||||
|
|
||||||
|
|
||||||
def test_complement_pitch_classes():
|
def test_complement_pitch_classes():
|
||||||
assert len(f520.complement) == 7
|
assert len(f520.complement) == 7
|
||||||
assert len(f520.complement.pitch_classes) == 7
|
assert len(f520.complement.pitch_classes) == 7
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue