subsequence 0.6.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. subsequence/__init__.py +231 -0
  2. subsequence/__main__.py +24 -0
  3. subsequence/assets/web/index.html +345 -0
  4. subsequence/cadences.py +113 -0
  5. subsequence/chord_graphs/__init__.py +100 -0
  6. subsequence/chord_graphs/aeolian_minor.py +158 -0
  7. subsequence/chord_graphs/chromatic_mediant.py +113 -0
  8. subsequence/chord_graphs/diminished.py +97 -0
  9. subsequence/chord_graphs/dorian_minor.py +127 -0
  10. subsequence/chord_graphs/functional_major.py +102 -0
  11. subsequence/chord_graphs/hooktheory_major.py +88 -0
  12. subsequence/chord_graphs/lydian_major.py +130 -0
  13. subsequence/chord_graphs/mixolydian.py +98 -0
  14. subsequence/chord_graphs/phrygian_minor.py +76 -0
  15. subsequence/chord_graphs/suspended.py +109 -0
  16. subsequence/chord_graphs/turnaround_global.py +157 -0
  17. subsequence/chord_graphs/whole_tone.py +77 -0
  18. subsequence/chords.py +419 -0
  19. subsequence/composition.py +6099 -0
  20. subsequence/conductor.py +238 -0
  21. subsequence/constants/__init__.py +24 -0
  22. subsequence/constants/durations.py +37 -0
  23. subsequence/constants/instruments/__init__.py +13 -0
  24. subsequence/constants/instruments/gm_cc.py +46 -0
  25. subsequence/constants/instruments/gm_drums.py +53 -0
  26. subsequence/constants/instruments/gm_instruments.py +32 -0
  27. subsequence/constants/instruments/roland_tr8s.py +320 -0
  28. subsequence/constants/instruments/vermona_drm1_drums.py +87 -0
  29. subsequence/constants/midi_notes.py +29 -0
  30. subsequence/constants/pulses.py +17 -0
  31. subsequence/constants/velocity.py +22 -0
  32. subsequence/definitions.py +232 -0
  33. subsequence/display.py +617 -0
  34. subsequence/easing.py +347 -0
  35. subsequence/event_emitter.py +109 -0
  36. subsequence/form_state.py +665 -0
  37. subsequence/forms.py +257 -0
  38. subsequence/groove.py +323 -0
  39. subsequence/harmonic_rhythm.py +83 -0
  40. subsequence/harmonic_state.py +352 -0
  41. subsequence/harmony.py +197 -0
  42. subsequence/held_notes.py +91 -0
  43. subsequence/helpers/__init__.py +0 -0
  44. subsequence/helpers/network.py +58 -0
  45. subsequence/helpers/wing.py +430 -0
  46. subsequence/intervals.py +436 -0
  47. subsequence/keystroke.py +249 -0
  48. subsequence/link_clock.py +128 -0
  49. subsequence/live_client.py +187 -0
  50. subsequence/live_reloader.py +298 -0
  51. subsequence/live_server.py +161 -0
  52. subsequence/melodic_state.py +483 -0
  53. subsequence/midi.py +97 -0
  54. subsequence/midi_utils.py +329 -0
  55. subsequence/mini_notation.py +164 -0
  56. subsequence/motifs.py +2356 -0
  57. subsequence/osc.py +194 -0
  58. subsequence/pattern.py +363 -0
  59. subsequence/pattern_algorithmic.py +2010 -0
  60. subsequence/pattern_builder.py +2589 -0
  61. subsequence/pattern_midi.py +1208 -0
  62. subsequence/progressions.py +1913 -0
  63. subsequence/py.typed +0 -0
  64. subsequence/roles.py +63 -0
  65. subsequence/sequence_utils.py +3123 -0
  66. subsequence/sequencer.py +2086 -0
  67. subsequence/tuning.py +453 -0
  68. subsequence/voicings.py +151 -0
  69. subsequence/web_ui.py +337 -0
  70. subsequence/weighted_graph.py +156 -0
  71. subsequence-0.6.4.dist-info/METADATA +208 -0
  72. subsequence-0.6.4.dist-info/RECORD +78 -0
  73. subsequence-0.6.4.dist-info/WHEEL +5 -0
  74. subsequence-0.6.4.dist-info/entry_points.txt +2 -0
  75. subsequence-0.6.4.dist-info/licenses/LICENSE +661 -0
  76. subsequence-0.6.4.dist-info/scm_file_list.json +193 -0
  77. subsequence-0.6.4.dist-info/scm_version.json +8 -0
  78. subsequence-0.6.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,113 @@
1
+ """Cadences — the curated formula table behind the producer cadence names.
2
+
3
+ A cadence is a two-chord tail formula plus a melodic close degree. The
4
+ producer names are primary — ``"strong"``, ``"soft"``, ``"open"``,
5
+ ``"fakeout"`` — with the theory names (authentic, plagal, half, deceptive)
6
+ accepted as aliases, per the standing rule: theory machinery under the hood,
7
+ producer words on the surface.
8
+
9
+ The table is pure data; the consumers wire it in:
10
+
11
+ - ``Progression.cadence(name)`` — tail substitution on a progression value.
12
+ - ``Progression.generate(cadence=)`` / ``freeze(cadence=)`` — the formula
13
+ becomes pins on the final bars of the constrained walk.
14
+ - ``Motif.generate(cadence=)`` — the close degree becomes ``end_on``.
15
+ - ``Composition.request_cadence()`` / ``section_cadence()`` — the live
16
+ clock steers its walk to arrive at the formula.
17
+ - ``sentence()`` / ``period()`` — the close degree aims the final unit.
18
+
19
+ Formula elements follow the progression-element grammar: ints are diatonic
20
+ degrees (quality inferred from key+scale at resolution time — ``4`` is IV
21
+ in major and iv in minor), roman strings carry their quality with them
22
+ (``"V"`` is the major dominant even in minor — the cadential convention).
23
+ """
24
+
25
+ import dataclasses
26
+ import typing
27
+
28
+
29
+ @dataclasses.dataclass(frozen=True)
30
+ class Cadence:
31
+
32
+ """One cadence formula — a named tail plus its melodic close.
33
+
34
+ Attributes:
35
+ name: The producer name (the primary key in the table).
36
+ theory_name: The traditional name, for the curious.
37
+ formula: The chord tail, in progression-element grammar, ending on
38
+ the arrival chord.
39
+ close_degree: The scale degree a melody lands on at this cadence
40
+ (1 for full closes; 5 for the open half — and 1 for the
41
+ fakeout too: the melody resolves as promised while the
42
+ harmony swerves, which is the trick of it).
43
+ """
44
+
45
+ name: str
46
+ theory_name: str
47
+ formula: typing.Tuple[typing.Any, ...]
48
+ close_degree: int
49
+
50
+
51
+ # The curated table — producer names primary. Two-chord tails throughout:
52
+ # a cadence is an arrival WITH its approach, and two chords is the smallest
53
+ # honest spelling of that.
54
+ CADENCES: typing.Dict[str, Cadence] = {
55
+ "strong": Cadence(
56
+ name = "strong",
57
+ theory_name = "authentic",
58
+ formula = ("V", 1),
59
+ close_degree = 1,
60
+ ),
61
+ "soft": Cadence(
62
+ name = "soft",
63
+ theory_name = "plagal",
64
+ formula = (4, 1),
65
+ close_degree = 1,
66
+ ),
67
+ "open": Cadence(
68
+ name = "open",
69
+ theory_name = "half",
70
+ formula = (4, "V"),
71
+ close_degree = 5,
72
+ ),
73
+ "fakeout": Cadence(
74
+ name = "fakeout",
75
+ theory_name = "deceptive",
76
+ formula = ("V", 6),
77
+ close_degree = 1,
78
+ ),
79
+ }
80
+
81
+ # Theory names as aliases — accuracy costs nothing here, the words name the
82
+ # same formulas.
83
+ _ALIASES: typing.Dict[str, str] = {
84
+ "authentic": "strong",
85
+ "perfect": "strong",
86
+ "plagal": "soft",
87
+ "half": "open",
88
+ "deceptive": "fakeout",
89
+ "interrupted": "fakeout",
90
+ }
91
+
92
+
93
+ def cadence_formula (name: str) -> Cadence:
94
+
95
+ """Look up a cadence by producer name or theory alias, loudly.
96
+
97
+ Raises:
98
+ ValueError: If the name is unknown — the error lists every valid
99
+ name and alias.
100
+ """
101
+
102
+ if not isinstance(name, str):
103
+ raise TypeError(f"a cadence is named by string, got {name!r}")
104
+
105
+ key = name.strip().lower()
106
+ key = _ALIASES.get(key, key)
107
+
108
+ if key not in CADENCES:
109
+ names = ", ".join(sorted(CADENCES))
110
+ aliases = ", ".join(sorted(_ALIASES))
111
+ raise ValueError(f"Unknown cadence {name!r}. Cadences: {names} (aliases: {aliases}).")
112
+
113
+ return CADENCES[key]
@@ -0,0 +1,100 @@
1
+ """
2
+ Chord graph builders for different harmonic transition models.
3
+ """
4
+
5
+ import abc
6
+ import typing
7
+
8
+ import subsequence.chords
9
+ import subsequence.intervals
10
+ import subsequence.weighted_graph
11
+
12
+
13
+ # Shared transition weights used by all chord graph builders.
14
+ WEIGHT_STRONG = 6
15
+ WEIGHT_MEDIUM = 4
16
+ WEIGHT_COMMON = 3
17
+ WEIGHT_DECEPTIVE = 2
18
+ WEIGHT_WEAK = 1
19
+
20
+
21
+ class ChordGraph (abc.ABC):
22
+
23
+ """Abstract base for chord transition graphs."""
24
+
25
+ @abc.abstractmethod
26
+ def build (self, key_name: str) -> typing.Tuple[subsequence.weighted_graph.WeightedGraph[subsequence.chords.Chord], subsequence.chords.Chord]:
27
+
28
+ """Build the weighted graph and return it with the tonic chord."""
29
+
30
+ ...
31
+
32
+ @abc.abstractmethod
33
+ def gravity_sets (self, key_name: str) -> typing.Tuple[typing.Set[subsequence.chords.Chord], typing.Set[subsequence.chords.Chord]]:
34
+
35
+ """Return (diatonic_set, functional_set) for key gravity weighting."""
36
+
37
+ ...
38
+
39
+
40
+ def validate_key_name (key_name: str) -> int:
41
+
42
+ """Validate a key name and return its pitch class.
43
+
44
+ Raises ValueError if the key name is not recognised.
45
+
46
+ Parameters:
47
+ key_name: Note name (e.g., ``"C"``, ``"F#"``, ``"Bb"``)
48
+
49
+ Returns:
50
+ Pitch class integer (0-11)
51
+ """
52
+
53
+ return subsequence.chords.key_name_to_pc(key_name)
54
+
55
+
56
+ def build_diatonic_chords (scale_pcs: typing.List[int], degree_qualities: typing.List[str]) -> typing.List[subsequence.chords.Chord]:
57
+
58
+ """Build chords for each scale degree.
59
+
60
+ Parameters:
61
+ scale_pcs: Pitch class for each degree (e.g., from ``scale_pitch_classes(key_pc, mode)``)
62
+ degree_qualities: Chord quality for each degree (e.g., ``["major", "minor", ...]``)
63
+
64
+ Returns:
65
+ List of Chord objects, one per scale degree
66
+ """
67
+
68
+ chords: typing.List[subsequence.chords.Chord] = []
69
+
70
+ for root_pc, quality in zip(scale_pcs, degree_qualities):
71
+ chords.append(subsequence.chords.Chord(root_pc=root_pc, quality=quality))
72
+
73
+ return chords
74
+
75
+
76
+ def _major_key_gravity_sets (key_name: str) -> typing.Tuple[typing.Set[subsequence.chords.Chord], typing.Set[subsequence.chords.Chord]]:
77
+
78
+ """Return diatonic and functional chord sets for a major key."""
79
+
80
+ key_pc = validate_key_name(key_name)
81
+ scale_pcs = subsequence.intervals.scale_pitch_classes(key_pc, "ionian")
82
+
83
+ chords = build_diatonic_chords(scale_pcs, subsequence.intervals.IONIAN_QUALITIES)
84
+ diatonic: typing.Set[subsequence.chords.Chord] = set(chords)
85
+
86
+ # Functional set: I, ii, V, V7.
87
+ function_intervals = [0, 2, 7]
88
+ function_qualities = ["major", "minor", "major"]
89
+
90
+ function_chords: typing.Set[subsequence.chords.Chord] = set()
91
+
92
+ for interval, quality in zip(function_intervals, function_qualities):
93
+ root_pc = (key_pc + interval) % 12
94
+ function_chords.add(subsequence.chords.Chord(root_pc=root_pc, quality=quality))
95
+
96
+ dominant_7th = subsequence.chords.Chord(root_pc=(key_pc + 7) % 12, quality="dominant_7th")
97
+ function_chords.add(dominant_7th)
98
+ diatonic.add(dominant_7th)
99
+
100
+ return diatonic, function_chords
@@ -0,0 +1,158 @@
1
+ import typing
2
+
3
+ import subsequence.chord_graphs
4
+ import subsequence.chords
5
+ import subsequence.intervals
6
+ import subsequence.weighted_graph
7
+
8
+
9
+ WEIGHT_STRONG = subsequence.chord_graphs.WEIGHT_STRONG
10
+ WEIGHT_MEDIUM = subsequence.chord_graphs.WEIGHT_MEDIUM
11
+ WEIGHT_COMMON = subsequence.chord_graphs.WEIGHT_COMMON
12
+ WEIGHT_DECEPTIVE = subsequence.chord_graphs.WEIGHT_DECEPTIVE
13
+ WEIGHT_WEAK = subsequence.chord_graphs.WEIGHT_WEAK
14
+
15
+ # Minor-specific weights.
16
+ WEIGHT_PHRYGIAN = 5
17
+ WEIGHT_PLAGAL = 4
18
+
19
+
20
+ class AeolianMinor (subsequence.chord_graphs.ChordGraph):
21
+
22
+ """Natural minor (Aeolian) graph with Phrygian and harmonic minor elements.
23
+
24
+ Focuses on the interplay between the natural minor scale and its
25
+ common variations:
26
+
27
+ - Aeolian (i, iv, v, bVI, bVII)
28
+ - Harmonic Minor (V, vii°) for stronger cadences
29
+ - Phrygian (bII) for tension
30
+
31
+ Suitable for a wide range of minor-key styles.
32
+ """
33
+
34
+ def __init__ (self, include_dominant_7th: bool = True) -> None:
35
+
36
+ """Configure whether to include dominant seventh chords."""
37
+
38
+ self.include_dominant_7th = include_dominant_7th
39
+
40
+ def build (self, key_name: str) -> typing.Tuple[subsequence.weighted_graph.WeightedGraph[subsequence.chords.Chord], subsequence.chords.Chord]:
41
+
42
+ """Build an Aeolian minor-key graph."""
43
+
44
+ key_pc = subsequence.chord_graphs.validate_key_name(key_name)
45
+
46
+ # Natural minor scale: 0, 2, 3, 5, 7, 8, 10
47
+ tonic = subsequence.chords.Chord(root_pc=key_pc, quality="minor")
48
+ supertonic_dim = subsequence.chords.Chord(root_pc=(key_pc + 2) % 12, quality="diminished")
49
+ mediant = subsequence.chords.Chord(root_pc=(key_pc + 3) % 12, quality="major")
50
+ subdominant = subsequence.chords.Chord(root_pc=(key_pc + 5) % 12, quality="minor")
51
+ natural_dominant = subsequence.chords.Chord(root_pc=(key_pc + 7) % 12, quality="minor")
52
+ submediant = subsequence.chords.Chord(root_pc=(key_pc + 8) % 12, quality="major")
53
+ subtonic = subsequence.chords.Chord(root_pc=(key_pc + 10) % 12, quality="major")
54
+
55
+ # Harmonic minor additions.
56
+ dominant = subsequence.chords.Chord(root_pc=(key_pc + 7) % 12, quality="major")
57
+ leading_dim = subsequence.chords.Chord(root_pc=(key_pc + 11) % 12, quality="diminished")
58
+
59
+ # Phrygian/Neapolitan flat-two.
60
+ flat_two = subsequence.chords.Chord(root_pc=(key_pc + 1) % 12, quality="major")
61
+
62
+ graph: subsequence.weighted_graph.WeightedGraph[subsequence.chords.Chord] = subsequence.weighted_graph.WeightedGraph()
63
+
64
+ # --- Tonic departures ---
65
+ graph.add_transition(tonic, subdominant, WEIGHT_PLAGAL)
66
+ graph.add_transition(tonic, submediant, WEIGHT_COMMON)
67
+ graph.add_transition(tonic, subtonic, WEIGHT_COMMON)
68
+ graph.add_transition(tonic, flat_two, WEIGHT_WEAK)
69
+ graph.add_transition(tonic, natural_dominant, WEIGHT_COMMON)
70
+
71
+ # --- Natural dominant (v) departures ---
72
+ graph.add_transition(natural_dominant, submediant, WEIGHT_COMMON)
73
+ graph.add_transition(natural_dominant, subdominant, WEIGHT_COMMON)
74
+ graph.add_transition(natural_dominant, tonic, WEIGHT_WEAK)
75
+
76
+ # --- Minor plagal ---
77
+ graph.add_transition(subdominant, tonic, WEIGHT_PLAGAL, label="soft")
78
+ graph.add_transition(subdominant, dominant, WEIGHT_MEDIUM, label="open")
79
+ graph.add_transition(subdominant, submediant, WEIGHT_WEAK)
80
+
81
+ # --- Aeolian cycle: i -> bVI -> bVII -> i ---
82
+ graph.add_transition(submediant, subtonic, WEIGHT_MEDIUM)
83
+ graph.add_transition(subtonic, tonic, WEIGHT_MEDIUM)
84
+
85
+ # --- Andalusian descent: i -> bVII -> bVI -> V ---
86
+ graph.add_transition(subtonic, submediant, WEIGHT_COMMON)
87
+ graph.add_transition(submediant, dominant, WEIGHT_MEDIUM)
88
+
89
+ # --- Phrygian cadence: bII -> i ---
90
+ graph.add_transition(flat_two, tonic, WEIGHT_PHRYGIAN)
91
+ graph.add_transition(flat_two, dominant, WEIGHT_COMMON)
92
+
93
+ # --- Harmonic minor cadence: V -> i ---
94
+ graph.add_transition(dominant, tonic, WEIGHT_STRONG, label="strong")
95
+ graph.add_transition(dominant, submediant, WEIGHT_DECEPTIVE, label="fakeout")
96
+
97
+ # --- Chromatic connectors ---
98
+ graph.add_transition(supertonic_dim, dominant, WEIGHT_MEDIUM)
99
+ graph.add_transition(leading_dim, tonic, WEIGHT_STRONG)
100
+ graph.add_transition(mediant, submediant, WEIGHT_COMMON)
101
+ graph.add_transition(mediant, subdominant, WEIGHT_WEAK)
102
+
103
+ # Reach the diatonic chords that previously had only outgoing edges, as
104
+ # occasional colour:
105
+ # i → bIII — the relative major, a staple minor-key move.
106
+ # iv → ii° — predominant into the supertonic dim (which then goes to V).
107
+ # iv → vii° — predominant into the harmonic-minor leading-tone triad (→ i).
108
+ graph.add_transition(tonic, mediant, WEIGHT_WEAK)
109
+ graph.add_transition(subdominant, supertonic_dim, WEIGHT_WEAK)
110
+ graph.add_transition(subdominant, leading_dim, WEIGHT_WEAK)
111
+
112
+ # --- Optional dominant seventh ---
113
+ if self.include_dominant_7th:
114
+ dominant_7th = subsequence.chords.Chord(root_pc=(key_pc + 7) % 12, quality="dominant_7th")
115
+
116
+ graph.add_transition(dominant, dominant_7th, WEIGHT_WEAK)
117
+ graph.add_transition(dominant_7th, tonic, WEIGHT_STRONG, label="strong")
118
+ graph.add_transition(dominant_7th, submediant, WEIGHT_DECEPTIVE, label="fakeout")
119
+
120
+ return graph, tonic
121
+
122
+ def gravity_sets (self, key_name: str) -> typing.Tuple[typing.Set[subsequence.chords.Chord], typing.Set[subsequence.chords.Chord]]:
123
+
124
+ """Return minor-key diatonic and functional chord sets."""
125
+
126
+ key_pc = subsequence.chord_graphs.validate_key_name(key_name)
127
+
128
+ diatonic: typing.Set[subsequence.chords.Chord] = set(
129
+ subsequence.chord_graphs.build_diatonic_chords(
130
+ subsequence.intervals.scale_pitch_classes(key_pc, "aeolian"),
131
+ subsequence.intervals.AEOLIAN_QUALITIES
132
+ )
133
+ )
134
+
135
+ # Harmonic minor V (major) and bII (Phrygian).
136
+ dominant_pc = (key_pc + 7) % 12
137
+ flat_two_pc = (key_pc + 1) % 12
138
+
139
+ diatonic.add(subsequence.chords.Chord(root_pc=dominant_pc, quality="major"))
140
+ diatonic.add(subsequence.chords.Chord(root_pc=flat_two_pc, quality="major"))
141
+
142
+ # Harmonic-minor leading-tone triad vii° — now reachable in the graph, so
143
+ # list it as a diatonic candidate for gravity weighting too.
144
+ diatonic.add(subsequence.chords.Chord(root_pc=(key_pc + 11) % 12, quality="diminished"))
145
+
146
+ dominant_7th = subsequence.chords.Chord(root_pc=dominant_pc, quality="dominant_7th")
147
+ diatonic.add(dominant_7th)
148
+
149
+ # Functional set: i, iv, V(/V7), bII.
150
+ functional: typing.Set[subsequence.chords.Chord] = set()
151
+
152
+ functional.add(subsequence.chords.Chord(root_pc=key_pc, quality="minor"))
153
+ functional.add(subsequence.chords.Chord(root_pc=(key_pc + 5) % 12, quality="minor"))
154
+ functional.add(subsequence.chords.Chord(root_pc=dominant_pc, quality="major"))
155
+ functional.add(subsequence.chords.Chord(root_pc=flat_two_pc, quality="major"))
156
+ functional.add(dominant_7th)
157
+
158
+ return diatonic, functional
@@ -0,0 +1,113 @@
1
+ import typing
2
+
3
+ import subsequence.chord_graphs
4
+ import subsequence.chords
5
+ import subsequence.weighted_graph
6
+
7
+
8
+ WEIGHT_MEDIUM = subsequence.chord_graphs.WEIGHT_MEDIUM
9
+ WEIGHT_COMMON = subsequence.chord_graphs.WEIGHT_COMMON
10
+ WEIGHT_WEAK = subsequence.chord_graphs.WEIGHT_WEAK
11
+
12
+ # Mediant-specific weight for the signature third-relation shifts.
13
+ WEIGHT_MEDIANT = 5
14
+
15
+
16
+ class ChromaticMediant (subsequence.chord_graphs.ChordGraph):
17
+
18
+ """Chromatic third-related harmony - roots move by major or minor thirds.
19
+
20
+ Many transitions connect chords whose roots are a major or minor third
21
+ apart (the characteristic motion); others are chromatic, fourth-, or
22
+ fifth-related, creating dramatic, colorful shifts that sound both surprising
23
+ and connected through shared common tones. No dominant-tonic functional
24
+ motion is used; the one subdominant-tonic edge is the minor-plagal
25
+ iv → I close from the subdominant-minor connector.
26
+
27
+ Good for cinematic, ambient, soundtrack, and experimental music.
28
+ """
29
+
30
+ def build (self, key_name: str) -> typing.Tuple[subsequence.weighted_graph.WeightedGraph[subsequence.chords.Chord], subsequence.chords.Chord]:
31
+
32
+ """Build a chromatic mediant graph with third-related root movement."""
33
+
34
+ key_pc = subsequence.chord_graphs.validate_key_name(key_name)
35
+
36
+ # Core chords - major and minor triads at third-related intervals.
37
+ tonic = subsequence.chords.Chord(root_pc=key_pc, quality="major")
38
+ flat_mediant = subsequence.chords.Chord(root_pc=(key_pc + 3) % 12, quality="major")
39
+ mediant = subsequence.chords.Chord(root_pc=(key_pc + 4) % 12, quality="major")
40
+ flat_submediant = subsequence.chords.Chord(root_pc=(key_pc + 8) % 12, quality="major")
41
+ submediant = subsequence.chords.Chord(root_pc=(key_pc + 9) % 12, quality="major")
42
+
43
+ # Minor variants for color contrast.
44
+ tonic_minor = subsequence.chords.Chord(root_pc=key_pc, quality="minor")
45
+ subdominant_minor = subsequence.chords.Chord(root_pc=(key_pc + 5) % 12, quality="minor")
46
+
47
+ graph: subsequence.weighted_graph.WeightedGraph[subsequence.chords.Chord] = subsequence.weighted_graph.WeightedGraph()
48
+
49
+ # --- Tonic departures (by thirds) ---
50
+ graph.add_transition(tonic, flat_mediant, WEIGHT_MEDIANT)
51
+ graph.add_transition(tonic, mediant, WEIGHT_MEDIANT)
52
+ graph.add_transition(tonic, flat_submediant, WEIGHT_COMMON)
53
+ graph.add_transition(tonic, submediant, WEIGHT_COMMON)
54
+ graph.add_transition(tonic, tonic_minor, WEIGHT_WEAK)
55
+
56
+ # --- Flat mediant (bIII) departures ---
57
+ graph.add_transition(flat_mediant, tonic, WEIGHT_MEDIANT)
58
+ graph.add_transition(flat_mediant, flat_submediant, WEIGHT_COMMON)
59
+ graph.add_transition(flat_mediant, submediant, WEIGHT_MEDIUM)
60
+
61
+ # --- Mediant (III) departures ---
62
+ graph.add_transition(mediant, tonic, WEIGHT_MEDIANT)
63
+ graph.add_transition(mediant, flat_submediant, WEIGHT_MEDIUM)
64
+ graph.add_transition(mediant, submediant, WEIGHT_COMMON)
65
+ graph.add_transition(mediant, flat_mediant, WEIGHT_WEAK)
66
+
67
+ # --- Flat submediant (bVI) departures ---
68
+ graph.add_transition(flat_submediant, tonic, WEIGHT_MEDIANT)
69
+ graph.add_transition(flat_submediant, mediant, WEIGHT_COMMON)
70
+ graph.add_transition(flat_submediant, flat_mediant, WEIGHT_COMMON)
71
+ graph.add_transition(flat_submediant, subdominant_minor, WEIGHT_WEAK)
72
+
73
+ # --- Submediant (VI) departures ---
74
+ graph.add_transition(submediant, tonic, WEIGHT_MEDIUM)
75
+ graph.add_transition(submediant, mediant, WEIGHT_COMMON)
76
+ graph.add_transition(submediant, flat_mediant, WEIGHT_COMMON)
77
+ graph.add_transition(submediant, flat_submediant, WEIGHT_WEAK)
78
+
79
+ # --- Tonic minor departures ---
80
+ graph.add_transition(tonic_minor, flat_mediant, WEIGHT_MEDIANT)
81
+ graph.add_transition(tonic_minor, flat_submediant, WEIGHT_COMMON)
82
+ graph.add_transition(tonic_minor, tonic, WEIGHT_MEDIUM)
83
+
84
+ # --- Subdominant minor (iv) connector ---
85
+ graph.add_transition(subdominant_minor, tonic, WEIGHT_MEDIUM)
86
+ graph.add_transition(subdominant_minor, flat_submediant, WEIGHT_COMMON)
87
+ graph.add_transition(subdominant_minor, flat_mediant, WEIGHT_WEAK)
88
+
89
+ return graph, tonic
90
+
91
+ def gravity_sets (self, key_name: str) -> typing.Tuple[typing.Set[subsequence.chords.Chord], typing.Set[subsequence.chords.Chord]]:
92
+
93
+ """Return chromatic mediant diatonic and functional chord sets."""
94
+
95
+ key_pc = subsequence.chord_graphs.validate_key_name(key_name)
96
+
97
+ # All chords in the graph form the "diatonic" set for gravity.
98
+ diatonic: typing.Set[subsequence.chords.Chord] = set()
99
+
100
+ for interval in [0, 3, 4, 8, 9]:
101
+ diatonic.add(subsequence.chords.Chord(root_pc=(key_pc + interval) % 12, quality="major"))
102
+
103
+ diatonic.add(subsequence.chords.Chord(root_pc=key_pc, quality="minor"))
104
+ diatonic.add(subsequence.chords.Chord(root_pc=(key_pc + 5) % 12, quality="minor"))
105
+
106
+ # Functional set: I, bIII, bVI (strongest mediant poles).
107
+ functional: typing.Set[subsequence.chords.Chord] = set()
108
+
109
+ functional.add(subsequence.chords.Chord(root_pc=key_pc, quality="major"))
110
+ functional.add(subsequence.chords.Chord(root_pc=(key_pc + 3) % 12, quality="major"))
111
+ functional.add(subsequence.chords.Chord(root_pc=(key_pc + 8) % 12, quality="major"))
112
+
113
+ return diatonic, functional
@@ -0,0 +1,97 @@
1
+ import typing
2
+
3
+ import subsequence.chord_graphs
4
+ import subsequence.chords
5
+ import subsequence.weighted_graph
6
+
7
+
8
+ WEIGHT_SYMMETRY = 5
9
+ WEIGHT_ESCAPE = 4
10
+ WEIGHT_RESOLVE = 4
11
+ WEIGHT_COMMON = subsequence.chord_graphs.WEIGHT_COMMON
12
+
13
+
14
+ class Diminished (subsequence.chord_graphs.ChordGraph):
15
+
16
+ """Diminished chord graph with minor-third symmetry and chromatic escapes.
17
+
18
+ Two chord types interlock:
19
+
20
+ - 4 diminished triads at roots 0, 3, 6, 9 (the symmetry backbone — the
21
+ minor-third cycle that also underpins the half-whole diminished scale)
22
+ - 4 dominant 7th chords at roots 1, 4, 7, 10 (escape chords)
23
+
24
+ Diminished chords connect to each other by minor thirds (the defining
25
+ rotation). Each dominant 7th sits a half step ABOVE its diminished
26
+ chord as a chromatic tension point — deliberately outside the
27
+ half-whole scale on the key root, which is what gives the escape its
28
+ lift before the half-step-down resolve. The result is angular,
29
+ disorienting, and cyclical - useful for dark techno, industrial, and
30
+ experimental electronic music.
31
+ """
32
+
33
+ def build (self, key_name: str) -> typing.Tuple[subsequence.weighted_graph.WeightedGraph[subsequence.chords.Chord], subsequence.chords.Chord]:
34
+
35
+ """Build an octatonic graph with diminished and dominant 7th chords."""
36
+
37
+ key_pc = subsequence.chord_graphs.validate_key_name(key_name)
38
+
39
+ # Four diminished triads, each a minor third apart.
40
+ dim_roots = [(key_pc + i) % 12 for i in [0, 3, 6, 9]]
41
+ dim_chords = [subsequence.chords.Chord(root_pc=r, quality="diminished") for r in dim_roots]
42
+
43
+ # Four dominant 7th chords, each a half step above a diminished chord.
44
+ dom_roots = [(key_pc + i) % 12 for i in [1, 4, 7, 10]]
45
+ dom_chords = [subsequence.chords.Chord(root_pc=r, quality="dominant_7th") for r in dom_roots]
46
+
47
+ graph: subsequence.weighted_graph.WeightedGraph[subsequence.chords.Chord] = subsequence.weighted_graph.WeightedGraph()
48
+
49
+ # --- Diminished ↔ diminished (minor third rotation) ---
50
+ for i, source in enumerate(dim_chords):
51
+ for j, target in enumerate(dim_chords):
52
+ if i != j:
53
+ graph.add_transition(source, target, WEIGHT_SYMMETRY)
54
+
55
+ # --- Diminished → dominant 7th (half step up = escape) ---
56
+ for i in range(4):
57
+ graph.add_transition(dim_chords[i], dom_chords[i], WEIGHT_ESCAPE)
58
+
59
+ # --- Dominant 7th → diminished (half step down = resolve) ---
60
+ for i in range(4):
61
+ graph.add_transition(dom_chords[i], dim_chords[i], WEIGHT_RESOLVE)
62
+
63
+ # --- Dominant 7th ↔ dominant 7th (minor third rotation) ---
64
+ for i, source in enumerate(dom_chords):
65
+ for j, target in enumerate(dom_chords):
66
+ if i != j:
67
+ graph.add_transition(source, target, WEIGHT_COMMON)
68
+
69
+ tonic = dim_chords[0]
70
+
71
+ return graph, tonic
72
+
73
+ def gravity_sets (self, key_name: str) -> typing.Tuple[typing.Set[subsequence.chords.Chord], typing.Set[subsequence.chords.Chord]]:
74
+
75
+ """Return octatonic diatonic and functional chord sets."""
76
+
77
+ key_pc = subsequence.chord_graphs.validate_key_name(key_name)
78
+
79
+ diatonic: typing.Set[subsequence.chords.Chord] = set()
80
+
81
+ # Gravity treats the full 8-chord palette as "home": the dominant
82
+ # escapes are included by design even though they sit outside the
83
+ # half-whole scale on the key root (they are the graph's tension
84
+ # vocabulary, not foreign territory to be damped).
85
+ for i in [0, 3, 6, 9]:
86
+ diatonic.add(subsequence.chords.Chord(root_pc=(key_pc + i) % 12, quality="diminished"))
87
+
88
+ for i in [1, 4, 7, 10]:
89
+ diatonic.add(subsequence.chords.Chord(root_pc=(key_pc + i) % 12, quality="dominant_7th"))
90
+
91
+ # Functional: the 4 diminished chords (symmetry backbone).
92
+ functional: typing.Set[subsequence.chords.Chord] = set()
93
+
94
+ for i in [0, 3, 6, 9]:
95
+ functional.add(subsequence.chords.Chord(root_pc=(key_pc + i) % 12, quality="diminished"))
96
+
97
+ return diatonic, functional