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.
- subsequence/__init__.py +231 -0
- subsequence/__main__.py +24 -0
- subsequence/assets/web/index.html +345 -0
- subsequence/cadences.py +113 -0
- subsequence/chord_graphs/__init__.py +100 -0
- subsequence/chord_graphs/aeolian_minor.py +158 -0
- subsequence/chord_graphs/chromatic_mediant.py +113 -0
- subsequence/chord_graphs/diminished.py +97 -0
- subsequence/chord_graphs/dorian_minor.py +127 -0
- subsequence/chord_graphs/functional_major.py +102 -0
- subsequence/chord_graphs/hooktheory_major.py +88 -0
- subsequence/chord_graphs/lydian_major.py +130 -0
- subsequence/chord_graphs/mixolydian.py +98 -0
- subsequence/chord_graphs/phrygian_minor.py +76 -0
- subsequence/chord_graphs/suspended.py +109 -0
- subsequence/chord_graphs/turnaround_global.py +157 -0
- subsequence/chord_graphs/whole_tone.py +77 -0
- subsequence/chords.py +419 -0
- subsequence/composition.py +6099 -0
- subsequence/conductor.py +238 -0
- subsequence/constants/__init__.py +24 -0
- subsequence/constants/durations.py +37 -0
- subsequence/constants/instruments/__init__.py +13 -0
- subsequence/constants/instruments/gm_cc.py +46 -0
- subsequence/constants/instruments/gm_drums.py +53 -0
- subsequence/constants/instruments/gm_instruments.py +32 -0
- subsequence/constants/instruments/roland_tr8s.py +320 -0
- subsequence/constants/instruments/vermona_drm1_drums.py +87 -0
- subsequence/constants/midi_notes.py +29 -0
- subsequence/constants/pulses.py +17 -0
- subsequence/constants/velocity.py +22 -0
- subsequence/definitions.py +232 -0
- subsequence/display.py +617 -0
- subsequence/easing.py +347 -0
- subsequence/event_emitter.py +109 -0
- subsequence/form_state.py +665 -0
- subsequence/forms.py +257 -0
- subsequence/groove.py +323 -0
- subsequence/harmonic_rhythm.py +83 -0
- subsequence/harmonic_state.py +352 -0
- subsequence/harmony.py +197 -0
- subsequence/held_notes.py +91 -0
- subsequence/helpers/__init__.py +0 -0
- subsequence/helpers/network.py +58 -0
- subsequence/helpers/wing.py +430 -0
- subsequence/intervals.py +436 -0
- subsequence/keystroke.py +249 -0
- subsequence/link_clock.py +128 -0
- subsequence/live_client.py +187 -0
- subsequence/live_reloader.py +298 -0
- subsequence/live_server.py +161 -0
- subsequence/melodic_state.py +483 -0
- subsequence/midi.py +97 -0
- subsequence/midi_utils.py +329 -0
- subsequence/mini_notation.py +164 -0
- subsequence/motifs.py +2356 -0
- subsequence/osc.py +194 -0
- subsequence/pattern.py +363 -0
- subsequence/pattern_algorithmic.py +2010 -0
- subsequence/pattern_builder.py +2589 -0
- subsequence/pattern_midi.py +1208 -0
- subsequence/progressions.py +1913 -0
- subsequence/py.typed +0 -0
- subsequence/roles.py +63 -0
- subsequence/sequence_utils.py +3123 -0
- subsequence/sequencer.py +2086 -0
- subsequence/tuning.py +453 -0
- subsequence/voicings.py +151 -0
- subsequence/web_ui.py +337 -0
- subsequence/weighted_graph.py +156 -0
- subsequence-0.6.4.dist-info/METADATA +208 -0
- subsequence-0.6.4.dist-info/RECORD +78 -0
- subsequence-0.6.4.dist-info/WHEEL +5 -0
- subsequence-0.6.4.dist-info/entry_points.txt +2 -0
- subsequence-0.6.4.dist-info/licenses/LICENSE +661 -0
- subsequence-0.6.4.dist-info/scm_file_list.json +193 -0
- subsequence-0.6.4.dist-info/scm_version.json +8 -0
- subsequence-0.6.4.dist-info/top_level.txt +1 -0
subsequence/cadences.py
ADDED
|
@@ -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
|