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,238 @@
1
+ """
2
+ Global automation signals — LFOs and ramps that modulate a composition over time.
3
+
4
+ The Conductor holds named, time-varying signals (swells, fades, filter sweeps)
5
+ that any pattern can read via ``p.signal(name)`` to shape velocity, CCs, or
6
+ note choices across long stretches of music.
7
+ """
8
+
9
+ import math
10
+ import typing
11
+ import warnings
12
+
13
+ import subsequence.easing
14
+
15
+
16
+ _VALID_LFO_SHAPES = frozenset({"sine", "triangle", "saw", "square"})
17
+
18
+
19
+ class Signal:
20
+
21
+ """
22
+ Abstract base class for a time-varying signal.
23
+ """
24
+
25
+ def value_at (self, beat: float) -> float:
26
+
27
+ """
28
+ Return the signal value at the given beat time. Subclasses must override.
29
+ """
30
+
31
+ raise NotImplementedError
32
+
33
+
34
+ class LFO(Signal):
35
+
36
+ """
37
+ A Low-Frequency Oscillator for generating periodic modulation signals.
38
+
39
+ LFOs are used to create cyclical changes (like a 'swell' or 'vibrato')
40
+ over many bars.
41
+ """
42
+
43
+ def __init__ (self, shape: str = "sine", cycle_beats: float = 16.0, min_val: float = 0.0, max_val: float = 1.0, phase: float = 0.0) -> None:
44
+
45
+ """
46
+ Initialize an LFO.
47
+
48
+ Parameters:
49
+ shape: The waveform shape ("sine", "triangle", "saw", "square").
50
+ cycle_beats: How many beats for one full cycle (e.g., 16.0 = 4 bars).
51
+ min_val: The bottom of the LFO range (default 0.0).
52
+ max_val: The top of the LFO range (default 1.0).
53
+ phase: Phase offset (0.0 to 1.0).
54
+ """
55
+
56
+ if cycle_beats <= 0:
57
+ raise ValueError("cycle_beats must be positive")
58
+
59
+ if shape not in _VALID_LFO_SHAPES:
60
+ raise ValueError(
61
+ f"Unknown LFO shape {shape!r}. "
62
+ f"Valid shapes: {sorted(_VALID_LFO_SHAPES)}"
63
+ )
64
+
65
+ self.shape = shape
66
+ self.cycle_beats = cycle_beats
67
+ self.min_val = min_val
68
+ self.max_val = max_val
69
+ self.phase = phase
70
+
71
+ def value_at (self, beat: float) -> float:
72
+
73
+ """
74
+ Compute the signal value at a given beat time.
75
+ """
76
+
77
+ progress = (beat / self.cycle_beats + self.phase) % 1.0
78
+ val = 0.0
79
+
80
+ if self.shape == "sine":
81
+ # Map -1..1 to 0..1
82
+ raw = math.sin(progress * 2 * math.pi)
83
+ val = (raw + 1) / 2
84
+
85
+ elif self.shape == "triangle":
86
+ # 0 -> 0.5 -> 1 -> 0.5 -> 0
87
+ if progress < 0.5:
88
+ val = progress * 2
89
+ else:
90
+ val = 2 - (progress * 2)
91
+
92
+ elif self.shape == "saw":
93
+ val = progress
94
+
95
+ else: # "square"
96
+ val = 1.0 if progress < 0.5 else 0.0
97
+
98
+ return self.min_val + (val * (self.max_val - self.min_val))
99
+
100
+
101
+ class Line(Signal):
102
+
103
+ """
104
+ A ramp signal that moves from one value to another over time.
105
+ """
106
+
107
+ def __init__ (self, start_val: float, end_val: float, duration_beats: float, start_beat: float = 0.0, loop: bool = False, shape: typing.Union[str, subsequence.easing.EasingFn] = "linear") -> None:
108
+
109
+ """
110
+ Initialize a ramp signal.
111
+
112
+ Parameters:
113
+ start_val: Initial value.
114
+ end_val: Target value.
115
+ duration_beats: How long it takes to reach the target (in beats).
116
+ start_beat: Global beat time when the ramp should start (default 0).
117
+ loop: Whether to jump back to start_val and repeat (default False).
118
+ shape: Easing curve — a name string (e.g. ``"ease_in_out"``) or any
119
+ callable that maps [0, 1] → [0, 1]. Defaults to ``"linear"``.
120
+ See :mod:`subsequence.easing` for available shapes.
121
+ """
122
+
123
+ if duration_beats <= 0:
124
+ raise ValueError("duration_beats must be positive")
125
+
126
+ self.start_val = start_val
127
+ self.end_val = end_val
128
+ self.duration_beats = duration_beats
129
+ self.start_beat = start_beat
130
+ self.loop = loop
131
+ self._easing_fn = subsequence.easing.get_easing(shape)
132
+
133
+ def value_at (self, beat: float) -> float:
134
+
135
+ """
136
+ Compute the ramp value at a given beat time.
137
+ """
138
+
139
+ elapsed = beat - self.start_beat
140
+
141
+ if elapsed < 0:
142
+ return self.start_val
143
+
144
+ if self.loop:
145
+ elapsed %= self.duration_beats
146
+ elif elapsed >= self.duration_beats:
147
+ return self.end_val
148
+
149
+ progress = elapsed / self.duration_beats
150
+ eased = self._easing_fn(progress)
151
+ return self.start_val + (eased * (self.end_val - self.start_val))
152
+
153
+
154
+ class Conductor:
155
+
156
+ """
157
+ A registry for global automation signals.
158
+
159
+ The ``Conductor`` allows you to define time-varying signals (like LFOs or
160
+ ramps) that are available to all pattern builders. This is ideal for
161
+ modulating parameters (like velocity or filter cutoff) over long
162
+ compositional timeframes.
163
+ """
164
+
165
+ def __init__ (self) -> None:
166
+
167
+ """
168
+ Create an empty conductor; register signals with lfo() or line().
169
+ """
170
+
171
+ self._signals: typing.Dict[str, Signal] = {}
172
+ self._warned_missing: typing.Set[str] = set()
173
+
174
+ @property
175
+ def signal_names (self) -> typing.Tuple[str, ...]:
176
+ """Names of all registered signals, in sorted order."""
177
+ return tuple(sorted(self._signals))
178
+
179
+ def lfo (self, name: str, shape: str = "sine", cycle_beats: float = 16.0, min_val: float = 0.0, max_val: float = 1.0, phase: float = 0.0) -> None:
180
+
181
+ """
182
+ Register a named LFO signal.
183
+
184
+ Example:
185
+ ```python
186
+ comp.conductor.lfo("swell", shape="sine", cycle_beats=32)
187
+ ```
188
+ """
189
+
190
+ self._signals[name] = LFO(shape, cycle_beats, min_val, max_val, phase)
191
+
192
+ def line (self, name: str, start_val: float, end_val: float, duration_beats: float, start_beat: float = 0.0, loop: bool = False, shape: typing.Union[str, subsequence.easing.EasingFn] = "linear") -> None:
193
+
194
+ """
195
+ Register a named ramp signal.
196
+
197
+ Parameters:
198
+ name: Signal name, used to retrieve the value via ``p.signal(name)``.
199
+ start_val: Value at the start of the ramp.
200
+ end_val: Value at the end of the ramp.
201
+ duration_beats: Duration of the ramp in beats.
202
+ start_beat: Beat time at which the ramp begins (default 0).
203
+ loop: If True, the ramp restarts from start_val after each cycle.
204
+ shape: Easing curve name or callable. Defaults to ``"linear"``.
205
+ Use ``"ease_in_out"`` for smooth crossfades, ``"exponential"``
206
+ for filter sweeps. See :mod:`subsequence.easing`.
207
+
208
+ Example:
209
+ ```python
210
+ comp.conductor.line("fadein", start_val=0, end_val=1, duration_beats=64)
211
+ comp.conductor.line("filter", start_val=0, end_val=1, duration_beats=32, shape="ease_in_out")
212
+ ```
213
+ """
214
+
215
+ self._signals[name] = Line(start_val, end_val, duration_beats, start_beat, loop, shape)
216
+
217
+ def get (self, name: str, beat: float) -> float:
218
+
219
+ """
220
+ Retrieve the value of a signal at a specific beat time.
221
+
222
+ Most patterns should use ``p.signal("name")`` instead, which
223
+ calls this method with the current bar time automatically.
224
+ Use ``get()`` directly when you need a beat time other than
225
+ the current bar.
226
+ """
227
+
228
+ if name not in self._signals:
229
+ if name not in self._warned_missing:
230
+ warnings.warn(
231
+ f"Conductor signal {name!r} not found; returning 0.0. "
232
+ "Register it with conductor.lfo() or conductor.line() first.",
233
+ stacklevel=2,
234
+ )
235
+ self._warned_missing.add(name)
236
+ return 0.0
237
+
238
+ return self._signals[name].value_at(beat)
@@ -0,0 +1,24 @@
1
+ """Constants for Subsequence.
2
+
3
+ This package contains the following sets of constants:
4
+
5
+ - ``subsequence.constants.pulses`` - Pulse-based MIDI timing (internal engine use)
6
+ - ``subsequence.constants.durations`` - Beat-based durations for pattern lengths and steps
7
+ - ``subsequence.constants.velocity`` - MIDI velocity constants
8
+ - ``subsequence.constants.instruments`` - Instrument-specific note maps (GM, Vermona, etc.)
9
+ - ``subsequence.constants.midi_notes`` - Named MIDI note constants C0–G9, C4 = 60 (Middle C)
10
+ - ``subsequence.constants.instruments.gm_cc`` - Named MIDI Continuous Controller (CC) numbers
11
+
12
+ Pulse constants are re-exported here for backwards compatibility, so
13
+ ``subsequence.constants.MIDI_QUARTER_NOTE`` continues to work.
14
+ """
15
+
16
+ # Re-export pulse constants for backwards compatibility.
17
+ # These match the values in subsequence.constants.pulses.
18
+
19
+ MIDI_THIRTYSECOND_NOTE = 3
20
+ MIDI_SIXTEENTH_NOTE = 6
21
+ MIDI_EIGHTH_NOTE = 12
22
+ MIDI_QUARTER_NOTE = 24
23
+ MIDI_HALF_NOTE = 48
24
+ MIDI_WHOLE_NOTE = 96
@@ -0,0 +1,37 @@
1
+ """Beat-based duration constants for pattern lengths and note timing.
2
+
3
+ All values are in **beats**, where 1.0 = one quarter note. Use these to express
4
+ pattern lengths and step sizes in musical terms instead of raw floats.
5
+
6
+ Multiply by a count for multi-note durations::
7
+
8
+ import subsequence.constants.durations as dur
9
+
10
+ # "9 sixteenth notes"
11
+ length = 9 * dur.SIXTEENTH # 2.25 beats
12
+
13
+ # "21 eighth notes"
14
+ length = 21 * dur.EIGHTH # 10.5 beats
15
+
16
+ # "4 bars of quarter notes"
17
+ length = 4 * dur.QUARTER # 4.0 beats
18
+
19
+ Use directly for step sizes and note durations::
20
+
21
+ p.arpeggio(tones, spacing=dur.SIXTEENTH, velocity=90)
22
+ p.arpeggio(tones, spacing=dur.DOTTED_SIXTEENTH, velocity=80)
23
+ p.repeat(60, spacing=dur.EIGHTH)
24
+ """
25
+
26
+ THIRTYSECOND = 0.125
27
+ SIXTEENTH = 0.25
28
+ DOTTED_SIXTEENTH = 0.375
29
+ TRIPLET_EIGHTH = 1 / 3
30
+ EIGHTH = 0.5
31
+ DOTTED_EIGHTH = 0.75
32
+ TRIPLET_QUARTER = 2 / 3
33
+ QUARTER = 1.0
34
+ DOTTED_QUARTER = 1.5
35
+ HALF = 2.0
36
+ DOTTED_HALF = 3.0
37
+ WHOLE = 4.0
@@ -0,0 +1,13 @@
1
+ """Instrument definitions — note maps, CC maps, and constants.
2
+
3
+ Each instrument module exports constants and lookup dictionaries that can be
4
+ passed to ``@composition.pattern()`` via ``drum_note_map`` and ``cc_name_map``.
5
+
6
+ Available instruments:
7
+
8
+ - ``subsequence.constants.instruments.gm_cc`` - General MIDI CC assignments
9
+ - ``subsequence.constants.instruments.gm_drums`` - General MIDI Level 1 drums
10
+ - ``subsequence.constants.instruments.gm_instruments`` - General MIDI Level 1 program numbers
11
+ - ``subsequence.constants.instruments.roland_tr8s`` - Roland TR-8S (drums + CCs)
12
+ - ``subsequence.constants.instruments.vermona_drm1_drums`` - Vermona DRM1 MKIV drums
13
+ """
@@ -0,0 +1,46 @@
1
+ """General MIDI Continuous Controller (CC) constants.
2
+
3
+ Standard MIDI CC assignments (0-127). These are supported by virtually all GM-compatible
4
+ instruments, synthesizers, and DAWs.
5
+
6
+ Two ways to use this module:
7
+
8
+ 1. **As constants** - reference CC numbers directly when sending MIDI CC messages::
9
+
10
+ import subsequence.constants.instruments.gm_cc as gm_cc
11
+
12
+ @composition.pattern(...)
13
+ def sweep (p):
14
+ p.cc(gm_cc.FILTER_CUTOFF, 127)
15
+
16
+ 2. **As a cc_name_map** - pass ``GM_CC_MAP`` to the ``cc_name_map`` parameter
17
+ of ``@composition.pattern()`` and use human-readable names like ``"filter_cutoff"``
18
+ or ``"sustain_pedal"`` in your CC calls::
19
+
20
+ import subsequence.constants.instruments.gm_cc as gm_cc
21
+
22
+ @composition.pattern(channel=1, beats=4, cc_name_map=gm_cc.GM_CC_MAP)
23
+ def synth (p):
24
+ p.cc("filter_cutoff", 100)
25
+ p.cc_ramp("expression", 0, 127, shape="ease_in")
26
+
27
+ Canonical source: `pymididefs <https://github.com/simonholliday/PyMidiDefs>`_.
28
+ """
29
+
30
+ import typing
31
+
32
+ # Re-export everything from pymididefs.cc — all CC constants and the lookup dict.
33
+ # These constants modules are deliberate re-export shims, exempt from the
34
+ # project's import-x-only rule (which the rest of the codebase satisfies).
35
+ from pymididefs.cc import * # noqa: F401,F403
36
+ from pymididefs.cc import CC_MAP # noqa: F401 — explicit re-export for type checkers
37
+
38
+ # ─── Backward-compatibility aliases ─────────────────────────────────────────
39
+ # Subsequence used FOOT_PEDAL_LSB; pymididefs uses FOOT_CONTROLLER_LSB.
40
+ FOOT_PEDAL_LSB = FOOT_CONTROLLER_LSB # noqa: F405
41
+
42
+ # GM_CC_MAP extends the canonical CC_MAP with the backward-compat alias.
43
+ GM_CC_MAP: typing.Dict[str, int] = {
44
+ **CC_MAP,
45
+ "foot_pedal_lsb": FOOT_PEDAL_LSB,
46
+ }
@@ -0,0 +1,53 @@
1
+ """General MIDI Level 1 drum note map.
2
+
3
+ Standard MIDI percussion assignments for channel 10 (0-indexed channel 9).
4
+ These note numbers are supported by virtually all GM-compatible instruments,
5
+ drum machines, and DAWs.
6
+
7
+ Two ways to use this module:
8
+
9
+ 1. **As a drum_note_map** - pass ``GM_DRUM_MAP`` to the ``drum_note_map`` parameter
10
+ of ``@composition.pattern()`` and use human-readable names like ``"kick"``,
11
+ ``"snare"``, or the numbered ``"kick_1"`` in your pattern builder calls::
12
+
13
+ import subsequence.constants.instruments.gm_drums
14
+
15
+ @composition.pattern(channel=10, beats=4, drum_note_map=subsequence.constants.instruments.gm_drums.GM_DRUM_MAP)
16
+ def drums (p):
17
+ p.hit_steps("kick", [0, 4, 8, 12], velocity=127)
18
+
19
+ 2. **As constants** - reference note numbers directly::
20
+
21
+ import subsequence.constants.instruments.gm_drums
22
+
23
+ @composition.pattern(channel=10, beats=4)
24
+ def drums (p):
25
+ p.hit_steps(subsequence.constants.instruments.gm_drums.KICK, [0, 4, 8, 12], velocity=127)
26
+
27
+ This map is the canonical GM percussion key map **plus** the unnumbered
28
+ "primary" aliases (``"kick"`` / ``"snare"`` / ``"crash"`` / ``"ride"`` → the
29
+ ``_1`` variant), so a pattern can use either ``"kick"`` or ``"kick_1"``. The
30
+ pure, one-name-per-note spec map is always available upstream as
31
+ ``pymididefs.drums.GM_DRUM_MAP``.
32
+
33
+ Canonical source: `pymididefs <https://github.com/simonholliday/PyMidiDefs>`_.
34
+ """
35
+
36
+ import typing
37
+
38
+ import pymididefs.drums
39
+
40
+ # Re-export every GM drum constant and the primary-alias names (KICK, SNARE,
41
+ # CRASH, RIDE, GM_DRUM_PRIMARY_ALIASES) so callers can write e.g.
42
+ # ``gm_drums.KICK`` or ``gm_drums.KICK_1``.
43
+ from pymididefs.drums import * # noqa: F401,F403
44
+
45
+ # The composition-facing map: the canonical GM Level 1 percussion key map plus
46
+ # the unnumbered primary aliases. Merged here (not upstream) so the spec map at
47
+ # ``pymididefs.drums.GM_DRUM_MAP`` stays one name per note. The annotated
48
+ # assignment deliberately specialises the spec map re-exported by ``import *``
49
+ # above (hence the no-redef suppression).
50
+ GM_DRUM_MAP: typing.Dict[str, int] = { # type: ignore[no-redef]
51
+ **pymididefs.drums.GM_DRUM_MAP,
52
+ **pymididefs.drums.GM_DRUM_PRIMARY_ALIASES,
53
+ }
@@ -0,0 +1,32 @@
1
+ """General MIDI Level 1 instrument program numbers.
2
+
3
+ All 128 GM Level 1 instrument assignments, organised into 16 families of 8.
4
+ Use these constants with ``p.program_change()`` to select instruments on
5
+ GM-compatible synthesizers and sound modules.
6
+
7
+ Usage::
8
+
9
+ import subsequence.constants.instruments.gm_instruments as gm
10
+
11
+ @composition.pattern(channel=1, length=4)
12
+ def strings (p):
13
+ p.program_change(gm.VIOLIN)
14
+ p.note(60, beat=0)
15
+
16
+ # Lookup by name
17
+ gm.GM_INSTRUMENT_MAP["flute"] # 73
18
+ gm.GM_INSTRUMENT_NAMES[73] # "Flute"
19
+
20
+ # Family ranges
21
+ gm.GM_FAMILIES["piano"] # (0, 7)
22
+
23
+ Canonical source: `pymididefs <https://github.com/simonholliday/PyMidiDefs>`_.
24
+ """
25
+
26
+ # Re-export everything from pymididefs.gm - all instrument constants and lookup tables.
27
+ from pymididefs.gm import * # noqa: F401,F403
28
+ from pymididefs.gm import ( # noqa: F401 - explicit re-exports for type checkers
29
+ GM_FAMILIES,
30
+ GM_INSTRUMENT_MAP,
31
+ GM_INSTRUMENT_NAMES,
32
+ )