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/__init__.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Subsequence - an algorithmic composition framework for Python.
|
|
4
|
+
|
|
5
|
+
Subsequence gives you a palette of mathematical building blocks -
|
|
6
|
+
Euclidean rhythms, cellular automata, L-systems, Markov chains,
|
|
7
|
+
cognitive melody generation - and a stateful engine that lets them
|
|
8
|
+
interact and evolve over time. Unlike tools that loop a fixed pattern
|
|
9
|
+
forever, Subsequence rebuilds every pattern fresh before each cycle
|
|
10
|
+
with full context, so algorithms feed into each other and compositions
|
|
11
|
+
emerge that no single technique could produce alone. It generates pure
|
|
12
|
+
MIDI (no audio engine) to control hardware synths, modular systems,
|
|
13
|
+
drum machines, or software VSTs/DAWs.
|
|
14
|
+
|
|
15
|
+
What makes it different:
|
|
16
|
+
|
|
17
|
+
- **A rich algorithmic palette.** Euclidean and Bresenham rhythm
|
|
18
|
+
generators, cellular automata (1D and 2D), L-system string rewriting,
|
|
19
|
+
Markov chains, cognitive melody via the Narmour model, probability-
|
|
20
|
+
weighted ghost notes, position-aware thinning, drones and continuous
|
|
21
|
+
notes, Perlin and pink noise, logistic chaos maps - plus groove
|
|
22
|
+
templates, velocity shaping, and pitch-bend automation to shape
|
|
23
|
+
how they sound.
|
|
24
|
+
- **Stateful patterns that evolve.** Each pattern is a Python function
|
|
25
|
+
rebuilt fresh every cycle with full context - current chord, section,
|
|
26
|
+
cycle count, shared data from other patterns. A Euclidean rhythm can
|
|
27
|
+
thin itself as tension builds, a cellular automaton can seed from the
|
|
28
|
+
harmony, and a Markov chain can shift behaviour between sections.
|
|
29
|
+
- **Optional chord graph.** Define weighted chord and key transitions
|
|
30
|
+
via probability graphs, with gravity and automatic voice leading.
|
|
31
|
+
A dozen built-in palettes and frozen progressions to lock some sections
|
|
32
|
+
while others evolve freely. Layer on cognitive harmony for
|
|
33
|
+
Narmour-based melodic inertia.
|
|
34
|
+
- **Sub-microsecond clock.** Hybrid sleep+spin timing achieves typical
|
|
35
|
+
pulse jitter of < 5 us on Linux, with zero long-term drift.
|
|
36
|
+
- **Turn anything into music.** ``composition.schedule()`` runs any
|
|
37
|
+
Python function on a beat cycle - APIs, sensors, files. Anything
|
|
38
|
+
Python can reach becomes a musical parameter.
|
|
39
|
+
- **Pure MIDI, zero sound engine.** No audio synthesis, no heavyweight
|
|
40
|
+
dependencies. Route to hardware synths, drum machines, Eurorack, or
|
|
41
|
+
software instruments.
|
|
42
|
+
|
|
43
|
+
Composition tools:
|
|
44
|
+
|
|
45
|
+
- **Rhythm and feel.** Euclidean and Bresenham generators, multi-voice
|
|
46
|
+
weighted Bresenham distribution (``bresenham_poly()``), ghost note
|
|
47
|
+
layers (``ghost_fill()``), position-aware note removal (``thin()`` -
|
|
48
|
+
the musical inverse of ``ghost_fill``), evolving cellular-automaton
|
|
49
|
+
rhythms (``cellular_1d()``, ``cellular_2d()``), smooth Perlin noise (``perlin_1d()``,
|
|
50
|
+
``perlin_2d()``, ``perlin_1d_sequence()``, ``perlin_2d_grid()``),
|
|
51
|
+
deterministic chaos sequences (``logistic_map()``), pink 1/f noise
|
|
52
|
+
(``pink_noise()``), L-system string rewriting (``p.lsystem()``),
|
|
53
|
+
Markov-chain generation (``p.markov()``), aperiodic binary rhythms
|
|
54
|
+
(``p.thue_morse()``), golden-ratio beat placement (``p.fibonacci()``),
|
|
55
|
+
Gray-Scott reaction-diffusion patterns (``p.reaction_diffusion()``),
|
|
56
|
+
Lorenz strange-attractor generation (``p.lorenz()``), exhaustive
|
|
57
|
+
pitch-subsequence melodies (``p.de_bruijn()``), step-wise melodies
|
|
58
|
+
with guaranteed pitch diversity (``p.self_avoiding_walk()``), drones
|
|
59
|
+
and explicit note on/off events (``p.drone()``, ``p.drone_off()``,
|
|
60
|
+
``p.silence()``),
|
|
61
|
+
groove templates (``Groove.swing()``, ``Groove.from_agr()``), swing via
|
|
62
|
+
``p.swing()`` (a shortcut for ``Groove.swing()``), randomize,
|
|
63
|
+
velocity shaping and ramps (``p.build_velocity_ramp()``), dropout, per-step
|
|
64
|
+
probability, and polyrhythms via independent pattern lengths.
|
|
65
|
+
- **Melody generation.** ``p.melody()`` with ``MelodicState`` applies
|
|
66
|
+
the Narmour Implication-Realization model to single-note lines:
|
|
67
|
+
continuation after small steps, reversal after large leaps, chord-tone
|
|
68
|
+
weighting, range gravity, and pitch-diversity penalty. History persists
|
|
69
|
+
across bar rebuilds for natural phrase continuity.
|
|
70
|
+
- **Chord parts.** ``comp.chords()`` and ``p.progression()`` play a chord
|
|
71
|
+
progression — generated from a chord-graph style or given explicitly — at a
|
|
72
|
+
declared *harmonic rhythm*: a fixed length, a shaped ``[WHOLE, HALF, HALF]``
|
|
73
|
+
sequence, or ``between(WHOLE, 3 * WHOLE, step=WHOLE)`` for chords of varying,
|
|
74
|
+
quantized length. Voicing density, ``detached`` articulation, and a seed for
|
|
75
|
+
a fixed phrase are all declarative.
|
|
76
|
+
- **Expression.** CC messages/ramps, pitch bend, note-correlated
|
|
77
|
+
bend/portamento/slide, program changes, SysEx, and OSC output - all
|
|
78
|
+
from within patterns.
|
|
79
|
+
- **Form and structure.** Musical form as a weighted graph, ordered list,
|
|
80
|
+
or generator. Patterns read ``p.section`` to adapt. Conductor signals
|
|
81
|
+
(LFOs, ramps) shape intensity over time.
|
|
82
|
+
- **Sequences as lists.** ``p.hit_steps("kick", [0, 4, 8, 12])`` and
|
|
83
|
+
``p.sequence(steps=..., pitches=..., velocities=...)`` place rhythms
|
|
84
|
+
and lines from plain Python lists - the vocabulary the generator and
|
|
85
|
+
density helpers in ``sequence_utils`` all speak.
|
|
86
|
+
- **Scales.** ``p.snap_to_scale()`` snaps notes to any
|
|
87
|
+
scale. ``scale_notes()`` generates a list of MIDI note numbers from
|
|
88
|
+
a key, mode, and range or note count - useful for arpeggios, Markov
|
|
89
|
+
chains, and melodic walks. Built-in western and non-western modes,
|
|
90
|
+
plus ``register_scale()`` for your own.
|
|
91
|
+
- **Microtonal tuning.** ``composition.tuning()`` applies a tuning
|
|
92
|
+
system globally; ``p.apply_tuning()`` overrides per-pattern.
|
|
93
|
+
Supports Scala ``.scl`` files, explicit cent lists, frequency ratios,
|
|
94
|
+
and N-TET equal temperaments. Polyphonic parts use explicit channel
|
|
95
|
+
rotation so simultaneous notes can carry independent pitch bends
|
|
96
|
+
without MPE. Compatible with any standard MIDI synthesiser.
|
|
97
|
+
- **Randomness tools.** Weighted choice, no-repeat shuffle, random
|
|
98
|
+
walk, probability gates. Deterministic seeding makes every decision
|
|
99
|
+
repeatable: set it composition-wide (``seed=42``) or per generator
|
|
100
|
+
(``seed=`` on any generator, with ``rng=`` for an explicit instance —
|
|
101
|
+
precedence ``rng`` > ``seed`` > the pattern's ``p.rng``). See the
|
|
102
|
+
README "Conventions" section for the API's shared vocabulary.
|
|
103
|
+
- **Pattern transforms.** Legato, detached, fixed gate (``p.duration()``),
|
|
104
|
+
reverse, time-stretch, rotate, transpose, invert, randomize, and
|
|
105
|
+
conditional ``p.every()``.
|
|
106
|
+
|
|
107
|
+
Integration:
|
|
108
|
+
|
|
109
|
+
- **MIDI clock.** Master (``clock_output()``) or follower
|
|
110
|
+
(``clock_follow=True``). When multiple inputs are connected, only
|
|
111
|
+
one may be designated as the master clock source; messages from
|
|
112
|
+
other inputs are filtered to prevent sync interference. Sync to a
|
|
113
|
+
DAW or drive hardware.
|
|
114
|
+
- **Latency compensation.** Declare each output device's physical
|
|
115
|
+
latency (``latency_ms=``); Subsequence delays the faster devices so
|
|
116
|
+
a mix of hardware and slower software instruments sound together.
|
|
117
|
+
- **MIDI mirroring with per-device drum maps.** Fan a pattern out to
|
|
118
|
+
extra ``(device, channel)`` destinations; an entry can carry its own
|
|
119
|
+
``drum_note_map`` so one named drum hit re-resolves to the right voice
|
|
120
|
+
on each device — a DRM1 and a General MIDI sampler alike.
|
|
121
|
+
- **Shared project definitions.** ``load_definitions("project.yaml")``
|
|
122
|
+
reads a small per-project YAML file naming notes, CCs, channels,
|
|
123
|
+
programs, and NRPNs — the same file the Subsample sampler reads — so
|
|
124
|
+
both tools use identical names and a renumber is a single edit.
|
|
125
|
+
- **Hardware control.** CC input mapping from knobs/faders to
|
|
126
|
+
``composition.data``; patterns read and write the same dict via
|
|
127
|
+
``p.data`` for both external data access and cross-pattern
|
|
128
|
+
communication. OSC for bidirectional communication with mixers,
|
|
129
|
+
lighting, visuals.
|
|
130
|
+
- **Live held-note arpeggiator.** ``composition.note_input()`` tracks the
|
|
131
|
+
notes you hold on a keyboard; a pattern reads them with ``p.held_notes()``
|
|
132
|
+
and arpeggiates them (``p.arpeggio(p.held_notes())``), with ``release_ms``
|
|
133
|
+
debounce and ``latch``. A performance layer over the deterministic
|
|
134
|
+
composition - empty when rendering headlessly.
|
|
135
|
+
- **Live coding.** Hot-swap patterns, change tempo, mute/unmute, and
|
|
136
|
+
tweak parameters during playback via a built-in TCP eval server.
|
|
137
|
+
- **Hotkeys.** Single keystrokes to jump sections, toggle mutes, or
|
|
138
|
+
fire any action - with optional bar-boundary quantization.
|
|
139
|
+
- **Real-time pattern triggering.** ``composition.trigger()`` generates
|
|
140
|
+
one-shot patterns in response to sensors, OSC, or any event.
|
|
141
|
+
- **Terminal display.** Live status line (BPM, bar, section, chord).
|
|
142
|
+
Add ``grid=True`` for an ASCII pattern grid showing velocity and
|
|
143
|
+
sustain - makes legato, detached, and staccato articulations visually
|
|
144
|
+
distinct at a glance.
|
|
145
|
+
Add ``grid_scale=2`` to zoom in horizontally, revealing swing and
|
|
146
|
+
groove micro-timing.
|
|
147
|
+
- **Web UI Dashboard (Beta).** Enable with ``composition.web_ui()`` to
|
|
148
|
+
broadcast live composition metadata and visualize piano-roll pattern
|
|
149
|
+
grids in a reactive HTTP/WebSocket browser dashboard.
|
|
150
|
+
- **Ableton Link.** Industry-standard wireless tempo/phase sync
|
|
151
|
+
(``comp.link()``; requires ``pip install subsequence[link]``).
|
|
152
|
+
Any Link-enabled app on the same LAN — Ableton Live, iOS synths,
|
|
153
|
+
other Subsequence instances — stays in time automatically.
|
|
154
|
+
- **Recording.** Record to standard MIDI file. Render to file without
|
|
155
|
+
waiting for real-time playback.
|
|
156
|
+
|
|
157
|
+
Minimal example:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
import subsequence
|
|
161
|
+
import subsequence.constants.instruments.gm_drums as gm_drums
|
|
162
|
+
|
|
163
|
+
comp = subsequence.Composition(bpm=120)
|
|
164
|
+
|
|
165
|
+
@comp.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
|
|
166
|
+
def drums (p):
|
|
167
|
+
(p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
|
|
168
|
+
.hit_steps("snare_1", [4, 12], velocity=90)
|
|
169
|
+
.hit_steps("hi_hat_closed", range(16), velocity=70))
|
|
170
|
+
|
|
171
|
+
comp.play()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Community and Feedback:
|
|
175
|
+
|
|
176
|
+
- **Discussions:** Chat and ask questions at https://github.com/simonholliday/subsequence/discussions
|
|
177
|
+
- **Issues:** Report bugs and request features at https://github.com/simonholliday/subsequence/issues
|
|
178
|
+
|
|
179
|
+
Package-level exports: ``Composition``, ``Chord``, ``Groove``, ``MelodicState``, ``Tuning``, ``Motif``, ``Phrase``, ``motif``, ``sentence``, ``period``, ``Degree``, ``ChordTone``, ``Approach``, ``MotifEvent``, ``ControlEvent``, ``Progression``, ``ChordSpan``, ``PitchSet``, ``progression``, ``Cadence``, ``Section``, ``Form``, ``roles``, ``sieve``, ``residual_class``, ``between``, ``parse_chord``, ``register_chord_quality``, ``register_scale``, ``scale_notes``, ``bank_select``, ``Definitions``, ``load_definitions``.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
import subsequence.cadences
|
|
183
|
+
import subsequence.chords
|
|
184
|
+
import subsequence.forms
|
|
185
|
+
import subsequence.roles
|
|
186
|
+
import subsequence.composition
|
|
187
|
+
import subsequence.definitions
|
|
188
|
+
import subsequence.groove
|
|
189
|
+
import subsequence.harmonic_rhythm
|
|
190
|
+
import subsequence.intervals
|
|
191
|
+
import subsequence.melodic_state
|
|
192
|
+
import subsequence.midi_utils
|
|
193
|
+
import subsequence.motifs
|
|
194
|
+
import subsequence.progressions
|
|
195
|
+
import subsequence.sequence_utils
|
|
196
|
+
import subsequence.tuning
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
Composition = subsequence.composition.Composition
|
|
200
|
+
Motif = subsequence.motifs.Motif
|
|
201
|
+
Phrase = subsequence.motifs.Phrase
|
|
202
|
+
motif = subsequence.motifs.motif
|
|
203
|
+
sentence = subsequence.motifs.sentence
|
|
204
|
+
period = subsequence.motifs.period
|
|
205
|
+
Cadence = subsequence.cadences.Cadence
|
|
206
|
+
Section = subsequence.forms.Section
|
|
207
|
+
Form = subsequence.forms.Form
|
|
208
|
+
Degree = subsequence.motifs.Degree
|
|
209
|
+
ChordTone = subsequence.motifs.ChordTone
|
|
210
|
+
Approach = subsequence.motifs.Approach
|
|
211
|
+
MotifEvent = subsequence.motifs.MotifEvent
|
|
212
|
+
ControlEvent = subsequence.motifs.ControlEvent
|
|
213
|
+
Progression = subsequence.progressions.Progression
|
|
214
|
+
ChordSpan = subsequence.progressions.ChordSpan
|
|
215
|
+
PitchSet = subsequence.progressions.PitchSet
|
|
216
|
+
progression = subsequence.progressions.progression
|
|
217
|
+
Chord = subsequence.chords.Chord
|
|
218
|
+
Groove = subsequence.groove.Groove
|
|
219
|
+
MelodicState = subsequence.melodic_state.MelodicState
|
|
220
|
+
Tuning = subsequence.tuning.Tuning
|
|
221
|
+
between = subsequence.harmonic_rhythm.between
|
|
222
|
+
parse_chord = subsequence.chords.parse_chord
|
|
223
|
+
register_chord_quality = subsequence.chords.register_chord_quality
|
|
224
|
+
register_scale = subsequence.intervals.register_scale
|
|
225
|
+
scale_notes = subsequence.intervals.scale_notes
|
|
226
|
+
bank_select = subsequence.midi_utils.bank_select
|
|
227
|
+
Definitions = subsequence.definitions.Definitions
|
|
228
|
+
load_definitions = subsequence.definitions.load_definitions
|
|
229
|
+
roles = subsequence.roles
|
|
230
|
+
sieve = subsequence.sequence_utils.sieve
|
|
231
|
+
residual_class = subsequence.sequence_utils.residual_class
|
subsequence/__main__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entry point for ``python -m subsequence`` — confirms the install and points to the docs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Configure logging
|
|
9
|
+
logging.basicConfig(level=logging.INFO)
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main () -> None:
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Main entry point for the subsequence module.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
logger.info("Subsequence module loaded.")
|
|
20
|
+
logger.info("To make your first sound, follow the Cookbook: https://subsequence.live/cookbook/00-setup.html")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Subsequence Dashboard</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg-color: #0d1117;
|
|
11
|
+
--text-color: #c9d1d9;
|
|
12
|
+
--accent-color: #58a6ff;
|
|
13
|
+
--panel-bg: #161b22;
|
|
14
|
+
--border-color: #30363d;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
background-color: var(--bg-color);
|
|
19
|
+
color: var(--text-color);
|
|
20
|
+
font-family: 'Inter', -apple-system, sans-serif;
|
|
21
|
+
margin: 0;
|
|
22
|
+
padding: 20px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.dashboard {
|
|
26
|
+
max-width: 1200px;
|
|
27
|
+
margin: 0 auto;
|
|
28
|
+
display: grid;
|
|
29
|
+
gap: 20px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.header {
|
|
33
|
+
display: flex;
|
|
34
|
+
justify-content: space-between;
|
|
35
|
+
align-items: center;
|
|
36
|
+
border-bottom: 1px solid var(--border-color);
|
|
37
|
+
padding-bottom: 20px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.header h1 {
|
|
41
|
+
margin: 0;
|
|
42
|
+
font-size: 1.5rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.status {
|
|
46
|
+
display: flex;
|
|
47
|
+
gap: 20px;
|
|
48
|
+
font-size: 1.2rem;
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.status span {
|
|
53
|
+
color: var(--accent-color);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.grid {
|
|
57
|
+
display: grid;
|
|
58
|
+
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
59
|
+
gap: 20px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.panel {
|
|
63
|
+
background: var(--panel-bg);
|
|
64
|
+
border: 1px solid var(--border-color);
|
|
65
|
+
border-radius: 8px;
|
|
66
|
+
padding: 20px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.panel h2 {
|
|
70
|
+
margin-top: 0;
|
|
71
|
+
font-size: 1.2rem;
|
|
72
|
+
border-bottom: 1px solid var(--border-color);
|
|
73
|
+
padding-bottom: 15px;
|
|
74
|
+
margin-bottom: 20px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.gauge-container {
|
|
78
|
+
margin-bottom: 20px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.gauge-label {
|
|
82
|
+
display: flex;
|
|
83
|
+
justify-content: space-between;
|
|
84
|
+
margin-bottom: 8px;
|
|
85
|
+
font-size: 0.95rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.gauge-bar {
|
|
89
|
+
width: 100%;
|
|
90
|
+
height: 12px;
|
|
91
|
+
background: var(--border-color);
|
|
92
|
+
border-radius: 6px;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
position: relative;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.gauge-fill {
|
|
98
|
+
height: 100%;
|
|
99
|
+
background: var(--accent-color);
|
|
100
|
+
transition: width 0.1s linear;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.pattern-grid-container {
|
|
104
|
+
margin-bottom: 15px;
|
|
105
|
+
background: rgba(0, 0, 0, 0.2);
|
|
106
|
+
border: 1px solid var(--border-color);
|
|
107
|
+
border-radius: 6px;
|
|
108
|
+
padding: 12px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.pattern-grid-container:last-child {
|
|
112
|
+
margin-bottom: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.pattern-grid-header {
|
|
116
|
+
display: flex;
|
|
117
|
+
justify-content: space-between;
|
|
118
|
+
margin-bottom: 10px;
|
|
119
|
+
font-family: monospace;
|
|
120
|
+
font-size: 1.1rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.pattern-grid {
|
|
124
|
+
position: relative;
|
|
125
|
+
width: 100%;
|
|
126
|
+
background: rgba(255, 255, 255, 0.03);
|
|
127
|
+
border-radius: 2px;
|
|
128
|
+
overflow: hidden;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.note-rect {
|
|
132
|
+
position: absolute;
|
|
133
|
+
border-radius: 2px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.piano-roll-label {
|
|
137
|
+
position: absolute;
|
|
138
|
+
left: 4px;
|
|
139
|
+
font-size: 0.70rem;
|
|
140
|
+
color: rgba(255, 255, 255, 0.6);
|
|
141
|
+
font-family: monospace;
|
|
142
|
+
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.7);
|
|
143
|
+
pointer-events: none;
|
|
144
|
+
z-index: 5;
|
|
145
|
+
white-space: nowrap;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.playhead {
|
|
149
|
+
position: absolute;
|
|
150
|
+
top: 0;
|
|
151
|
+
bottom: 0;
|
|
152
|
+
width: 1px;
|
|
153
|
+
background-color: rgba(255, 255, 255, 0.8);
|
|
154
|
+
box-shadow: 0 0 4px rgba(255, 255, 255, 0.5);
|
|
155
|
+
z-index: 10;
|
|
156
|
+
}
|
|
157
|
+
</style>
|
|
158
|
+
</head>
|
|
159
|
+
|
|
160
|
+
<body>
|
|
161
|
+
<div id="app"></div>
|
|
162
|
+
|
|
163
|
+
<script type="module">
|
|
164
|
+
import { h, Component, render } from 'https://esm.sh/preact@10.19.6';
|
|
165
|
+
import { useState, useEffect } from 'https://esm.sh/preact@10.19.6/hooks';
|
|
166
|
+
import htm from 'https://esm.sh/htm@3.1.1';
|
|
167
|
+
|
|
168
|
+
const html = htm.bind(h);
|
|
169
|
+
|
|
170
|
+
function Transport({ state }) {
|
|
171
|
+
let sectionStr = state.section || '---';
|
|
172
|
+
if (state.section && state.section_bars) {
|
|
173
|
+
sectionStr = `[${state.section} ${state.section_bar}/${state.section_bars}`;
|
|
174
|
+
if (state.next_section) {
|
|
175
|
+
sectionStr += ` \u2192 ${state.next_section}`;
|
|
176
|
+
}
|
|
177
|
+
sectionStr += ']';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return html`
|
|
181
|
+
<div class="header">
|
|
182
|
+
<h1>Subsequence <span>Dashboard</span></h1>
|
|
183
|
+
<div class="status">
|
|
184
|
+
<div>BPM: <span>${state.bpm ? state.bpm.toFixed(1) : '---'}</span></div>
|
|
185
|
+
${state.key ? html`<div>Key: <span>${state.key}</span></div>` : ''}
|
|
186
|
+
<div>Bar: <span>${state.global_bar || 0}.${state.global_beat || 0}</span></div>
|
|
187
|
+
<div>Section: <span>${sectionStr}</span></div>
|
|
188
|
+
<div>Chord: <span>${state.chord || '---'}</span></div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function SignalGauge({ name, value }) {
|
|
195
|
+
// value is expected to be roughly 0.0 - 1.0. Clamp it to 0-100% just in case
|
|
196
|
+
const pct = Math.max(0, Math.min(100, value * 100));
|
|
197
|
+
return html`
|
|
198
|
+
<div class="gauge-container">
|
|
199
|
+
<div class="gauge-label">
|
|
200
|
+
<span>${name}</span>
|
|
201
|
+
<span>${value.toFixed(2)}</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="gauge-bar">
|
|
204
|
+
<div class="gauge-fill" style="width: ${pct}%"></div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function PatternGrid({ pattern, state }) {
|
|
211
|
+
const color = pattern.muted ? '#ff7b72' : '#3fb950';
|
|
212
|
+
const notes = pattern.notes || [];
|
|
213
|
+
const length_pulses = pattern.length_pulses || 1;
|
|
214
|
+
const drum_map = pattern.drum_map || {};
|
|
215
|
+
|
|
216
|
+
// Reverse the drum map: { midi_note: "kick" }
|
|
217
|
+
const reverseDrumMap = {};
|
|
218
|
+
for (const [name, noteNum] of Object.entries(drum_map)) {
|
|
219
|
+
if (!reverseDrumMap[noteNum]) reverseDrumMap[noteNum] = name;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Find unique pitches and sort descending (higher pitches at top)
|
|
223
|
+
const pitches = [...new Set(notes.map(n => n.p))].sort((a, b) => b - a);
|
|
224
|
+
const rowHeight = 16;
|
|
225
|
+
const gridHeight = Math.max(48, pitches.length * rowHeight);
|
|
226
|
+
|
|
227
|
+
const pitchToY = (p) => {
|
|
228
|
+
const idx = pitches.indexOf(p);
|
|
229
|
+
if (idx === -1) return 0;
|
|
230
|
+
// Center the note vertically in its "row"
|
|
231
|
+
return idx * rowHeight + 1;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const getPitchName = (p) => {
|
|
235
|
+
if (reverseDrumMap[p]) return reverseDrumMap[p];
|
|
236
|
+
const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
237
|
+
const octave = Math.floor(p / 12) - 1;
|
|
238
|
+
const note = noteNames[p % 12];
|
|
239
|
+
return `${note}${octave}`;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const localPlayhead = (state.playhead_pulse || 0) % length_pulses;
|
|
243
|
+
const playheadLeft = (localPlayhead / length_pulses) * 100;
|
|
244
|
+
|
|
245
|
+
const renderLabel = (p) => {
|
|
246
|
+
const top = pitchToY(p);
|
|
247
|
+
return html`<div class="piano-roll-label" style="top: ${top}px; line-height: ${rowHeight - 2}px;">${getPitchName(p)}</div>`;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const renderNote = (n, i) => {
|
|
251
|
+
const left = (n.s / length_pulses) * 100;
|
|
252
|
+
// Clamp width to remainder of bar so it doesn't spill over
|
|
253
|
+
const width = Math.min((n.d / length_pulses) * 100, 100 - left);
|
|
254
|
+
const top = pitchToY(n.p);
|
|
255
|
+
const opacity = Math.max(0.3, n.v / 127);
|
|
256
|
+
return html`<div key=${i} class="note-rect" style="left: ${left}%; width: ${Math.max(0.5, width)}%; top: ${top}px; height: ${rowHeight - 2}px; opacity: ${opacity}; background-color: ${color};"></div>`;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return html`
|
|
260
|
+
<div class="pattern-grid-container">
|
|
261
|
+
<div class="pattern-grid-header">
|
|
262
|
+
<span>${pattern.name}</span>
|
|
263
|
+
<span style="color: ${color}; font-weight: bold; font-size: 0.85rem;">${pattern.muted ? 'MUTED' : 'PLAYING'}</span>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="pattern-grid" style="height: ${gridHeight}px;">
|
|
266
|
+
${pitches.map(renderLabel)}
|
|
267
|
+
${notes.map(renderNote)}
|
|
268
|
+
<div class="playhead" style="left: ${playheadLeft}%;"></div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function App() {
|
|
275
|
+
const [state, setState] = useState({
|
|
276
|
+
bpm: null,
|
|
277
|
+
chord: null,
|
|
278
|
+
section: null,
|
|
279
|
+
patterns: [],
|
|
280
|
+
signals: {}
|
|
281
|
+
});
|
|
282
|
+
const [connected, setConnected] = useState(false);
|
|
283
|
+
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
let ws;
|
|
286
|
+
|
|
287
|
+
function connect() {
|
|
288
|
+
// __WS_PORT__ is substituted by web_ui.py at serve time so
|
|
289
|
+
// WebUI(ws_port=...) actually reaches the dashboard; the
|
|
290
|
+
// regex guard keeps the raw file working (default 8765)
|
|
291
|
+
// when opened without the server.
|
|
292
|
+
const wsPortToken = '__WS_PORT__';
|
|
293
|
+
const wsPort = /^\d+$/.test(wsPortToken) ? wsPortToken : '8765';
|
|
294
|
+
ws = new WebSocket('ws://' + window.location.hostname + ':' + wsPort);
|
|
295
|
+
|
|
296
|
+
ws.onopen = () => setConnected(true);
|
|
297
|
+
|
|
298
|
+
ws.onmessage = (event) => {
|
|
299
|
+
const data = JSON.parse(event.data);
|
|
300
|
+
setState(data);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
ws.onclose = () => {
|
|
304
|
+
setConnected(false);
|
|
305
|
+
// Try to reconnect in 2s
|
|
306
|
+
setTimeout(connect, 2000);
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
connect();
|
|
311
|
+
|
|
312
|
+
return () => {
|
|
313
|
+
if (ws) ws.close();
|
|
314
|
+
};
|
|
315
|
+
}, []);
|
|
316
|
+
|
|
317
|
+
return html`
|
|
318
|
+
<div class="dashboard">
|
|
319
|
+
<${Transport} state=${state} />
|
|
320
|
+
|
|
321
|
+
${!connected ? html`<div style="color: #ff7b72; padding: 20px; text-align: center; border: 1px solid #ff7b72; border-radius: 8px;">Not connected to Subsequence websocket...</div>` : null}
|
|
322
|
+
|
|
323
|
+
<div class="grid">
|
|
324
|
+
<div class="panel">
|
|
325
|
+
<h2>Signals & Conductor</h2>
|
|
326
|
+
${Object.keys(state.signals).length === 0 ? html`<p style="color: grey; font-style: italic;">No active signals</p>` : null}
|
|
327
|
+
${Object.entries(state.signals).map(([name, value]) =>
|
|
328
|
+
html`<${SignalGauge} key=${name} name=${name} value=${value} />`
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
<div class="panel">
|
|
332
|
+
<h2>Active Patterns</h2>
|
|
333
|
+
${state.patterns.length === 0 ? html`<p style="color: grey; font-style: italic;">No active patterns</p>` : null}
|
|
334
|
+
${state.patterns.map(p => html`<${PatternGrid} key=${p.name} pattern=${p} state=${state} />`)}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
render(html`<${App} />`, document.getElementById('app'));
|
|
342
|
+
</script>
|
|
343
|
+
</body>
|
|
344
|
+
|
|
345
|
+
</html>
|