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,430 @@
1
+ """Behringer WING mixer helper.
2
+
3
+ NOTE: This module is specific to Behringer WING series mixers (WING, WING Rack,
4
+ WING Compact). It is NOT part of the core Subsequence module and is provided
5
+ as a convenience for users who want to integrate WING discovery and address
6
+ exploration with their Subsequence compositions.
7
+
8
+ OSC port: The WING listens on UDP **port 2223** for all OSC traffic.
9
+
10
+ Typical workflow
11
+ ----------------
12
+ ::
13
+
14
+ import subsequence
15
+ import subsequence.helpers.wing as wing
16
+
17
+ # 1. Find the mixer on the LAN
18
+ device = wing.discover()
19
+ if device is None:
20
+ raise RuntimeError("No WING found on the network")
21
+
22
+ ip = device["ip"]
23
+ print(f"Found {device['model']} at {ip} (firmware {device['firmware']})")
24
+
25
+ # 2. Tell Subsequence to send OSC to it
26
+ composition.osc(send_port=wing.WING_PORT, send_host=ip)
27
+
28
+ # 3. In your patterns, use standard p.osc() / p.osc_ramp() with WING addresses
29
+ @composition.pattern(channel=0, length=4)
30
+ def mixer (p):
31
+ p.osc_ramp("/ch/1/fdr", 0.0, 0.75, shape="ease_in") # fade up channel 1
32
+
33
+ # 4. Explore available addresses at development time
34
+ wing.print_node(ip, "/ch/1") # list all parameters under channel 1
35
+ wing.print_node(ip, "/ch/1/fdr") # inspect the fader leaf value
36
+
37
+ CLI usage
38
+ ---------
39
+ ::
40
+
41
+ # Discover — prints the device info
42
+ python -m subsequence.helpers.wing
43
+
44
+ # Query a node — pretty-prints its structure / value
45
+ python -m subsequence.helpers.wing /ch/1
46
+ python -m subsequence.helpers.wing /ch/1/fdr
47
+ python -m subsequence.helpers.wing /
48
+
49
+ Address conventions
50
+ -------------------
51
+ Addresses use the WING's internal node tree. Key top-level nodes::
52
+
53
+ /ch/1..40 Input channels
54
+ /aux/1..8 Aux inputs
55
+ /bus/1..16 Mix buses
56
+ /main/lr Main L/R bus
57
+ /main/m Mono / centre bus
58
+ /mtx/1..8 Matrix outputs
59
+ /fx/1..8 FX returns
60
+ /dca/1..8 DCA groups
61
+ /mgrp/1..8 Mute groups
62
+
63
+ Useful leaf addresses per channel (e.g. ``/ch/1/…``)::
64
+
65
+ fdr Fader level, 0.0 (−∞) .. 1.0 (≈+10 dB)
66
+ pan Pan, 0.0 (full L) .. 1.0 (full R), 0.5 = centre
67
+ mute Mute, 0 = unmuted, 1 = muted
68
+ name Channel name (string)
69
+ col Colour index (int)
70
+ """
71
+
72
+ import logging
73
+ import socket
74
+ import sys
75
+ import typing
76
+
77
+ import pythonosc.osc_message
78
+ import pythonosc.osc_message_builder
79
+
80
+ import subsequence.helpers.network
81
+
82
+
83
+ logger = logging.getLogger(__name__)
84
+
85
+
86
+ WING_PORT: int = 2223
87
+ """Default UDP port for the Behringer WING."""
88
+
89
+ _BROADCAST_ADDRS: typing.List[str] = ["255.255.255.255"]
90
+ """Broadcast addresses tried by :func:`discover`. Supplemented at runtime with
91
+ subnet-specific addresses derived from local interfaces."""
92
+
93
+
94
+ # ── Internal helpers ──────────────────────────────────────────────────────────
95
+
96
+
97
+ def _build_osc (address: str) -> bytes:
98
+ """Build a no-argument OSC message for *address*."""
99
+ return pythonosc.osc_message_builder.OscMessageBuilder(address=address).build().dgram
100
+
101
+
102
+ def _parse_osc (data: bytes) -> typing.Optional[pythonosc.osc_message.OscMessage]:
103
+ """Parse raw bytes into an OscMessage, returning None on failure."""
104
+ try:
105
+ return pythonosc.osc_message.OscMessage(data)
106
+ except Exception:
107
+ return None
108
+
109
+
110
+ def _classify (params: typing.List[typing.Any]) -> str:
111
+ """Return 'node' or 'leaf' based on the response param types.
112
+
113
+ - Any float or int in the params → numeric leaf.
114
+ - All strings, more than one → node (directory listing of children).
115
+ - Single string → string leaf.
116
+ """
117
+ if not params:
118
+ return "leaf"
119
+ if any(isinstance(p, (float, int)) and not isinstance(p, bool) for p in params):
120
+ return "leaf"
121
+ if len(params) > 1:
122
+ return "node"
123
+ return "leaf"
124
+
125
+
126
+ # ── Public API ────────────────────────────────────────────────────────────────
127
+
128
+
129
+ def discover (
130
+ port: int = WING_PORT,
131
+ timeout: float = 2.0,
132
+ ) -> typing.Optional[typing.Dict[str, str]]:
133
+ """Auto-discover a Behringer WING on the local network.
134
+
135
+ Sends an OSC ``/?`` broadcast to UDP port *port* and waits for a reply.
136
+ The WING responds with a comma-separated info string containing its IP,
137
+ model, and firmware version.
138
+
139
+ Parameters:
140
+ port: UDP port to broadcast on (default 2223 — the WING's OSC port).
141
+ timeout: Seconds to wait for a reply.
142
+
143
+ Returns:
144
+ A ``dict`` with keys ``ip``, ``device``, ``model``, ``form_factor``,
145
+ ``firmware``, or ``None`` if no device replied.
146
+
147
+ Example::
148
+
149
+ device = wing.discover()
150
+ if device:
151
+ print(device["ip"]) # "192.168.0.116"
152
+ print(device["firmware"]) # "3.1-0-g9f314617:release"
153
+ """
154
+ dgram = _build_osc("/?")
155
+
156
+ # Build list of broadcast addresses to try: global first, then subnet-specific
157
+ broadcasts = list(_BROADCAST_ADDRS) + subsequence.helpers.network.get_local_broadcasts()
158
+ # Deduplicate while preserving order
159
+ seen: typing.Set[str] = set()
160
+ unique_broadcasts: typing.List[str] = []
161
+ for b in broadcasts:
162
+ if b not in seen:
163
+ seen.add(b)
164
+ unique_broadcasts.append(b)
165
+
166
+ for broadcast in unique_broadcasts:
167
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
168
+ try:
169
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
170
+ sock.settimeout(timeout / len(unique_broadcasts))
171
+ sock.sendto(dgram, (broadcast, port))
172
+ try:
173
+ data, addr = sock.recvfrom(4096)
174
+ except socket.timeout:
175
+ continue
176
+ except OSError as e:
177
+ # The docstring promises None when nothing replies — a machine
178
+ # with no route for this broadcast (offline laptop, ENETUNREACH)
179
+ # is "nothing replied", not a crash. Try the next address.
180
+ logger.debug(f"WING discovery broadcast to {broadcast} failed: {e}")
181
+ continue
182
+ finally:
183
+ sock.close()
184
+
185
+ msg = _parse_osc(data)
186
+ if msg is None:
187
+ continue
188
+
189
+ params = list(msg.params)
190
+ if not params or not isinstance(params[0], str):
191
+ continue
192
+
193
+ # Response: "WING,192.168.0.116,WING-PP-20021049,wing-rack,ID,firmware"
194
+ fields = params[0].split(",")
195
+ result: typing.Dict[str, str] = {
196
+ "ip": addr[0],
197
+ "device": fields[0] if len(fields) > 0 else "",
198
+ "model": fields[2] if len(fields) > 2 else "",
199
+ "form_factor": fields[3] if len(fields) > 3 else "",
200
+ "firmware": fields[5] if len(fields) > 5 else "",
201
+ }
202
+ return result
203
+
204
+ return None
205
+
206
+
207
+ def query (
208
+ host: str,
209
+ address: str,
210
+ port: int = WING_PORT,
211
+ timeout: float = 2.0,
212
+ ) -> typing.Optional[typing.Dict[str, typing.Any]]:
213
+ """Query a node or leaf address on the WING and return a structured dict.
214
+
215
+ Sends an argument-less OSC message to *address*. The WING responds with
216
+ either a list of child node names (for a node address) or the current value
217
+ of a parameter (for a leaf address).
218
+
219
+ Parameters:
220
+ host: WING IP address (e.g. ``"192.168.0.116"``).
221
+ address: OSC address to query (e.g. ``"/ch/1"`` or ``"/ch/1/fdr"``).
222
+ port: UDP port (default 2223).
223
+ timeout: Seconds to wait for a reply.
224
+
225
+ Returns:
226
+ A ``dict`` with keys:
227
+
228
+ - ``address`` (str): The OSC address of the response.
229
+ - ``type`` (str): ``"node"`` or ``"leaf"``.
230
+ - For **nodes**: ``children`` (list[str]) — child node names.
231
+ - For **string leaves**: ``value`` (str).
232
+ - For **numeric leaves**: ``value`` (str), ``value_f`` (float|None),
233
+ ``value_i`` (int|None).
234
+
235
+ Returns ``None`` on timeout.
236
+
237
+ Example::
238
+
239
+ result = wing.query(ip, "/ch/1/fdr")
240
+ # {'address': '/ch/1/fdr', 'type': 'leaf', 'value': '0.0',
241
+ # 'value_f': 0.75, 'value_i': 0}
242
+
243
+ result = wing.query(ip, "/ch/1")
244
+ # {'address': '/ch/1', 'type': 'node',
245
+ # 'children': ['in', 'flt', 'clink', 'col', 'name', 'fdr', ...]}
246
+ """
247
+ dgram = _build_osc(address)
248
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
249
+ try:
250
+ sock.settimeout(timeout)
251
+ sock.sendto(dgram, (host, port))
252
+ try:
253
+ data, _ = sock.recvfrom(65535)
254
+ except socket.timeout:
255
+ return None
256
+ finally:
257
+ sock.close()
258
+
259
+ msg = _parse_osc(data)
260
+ if msg is None:
261
+ return None
262
+
263
+ params = list(msg.params)
264
+ kind = _classify(params)
265
+
266
+ result: typing.Dict[str, typing.Any] = {
267
+ "address": msg.address,
268
+ "type": kind,
269
+ }
270
+
271
+ if kind == "node":
272
+ result["children"] = [str(p) for p in params]
273
+ else:
274
+ # Leaf — extract typed values
275
+ result["value"] = str(params[0]) if params else ""
276
+ floats = [p for p in params if isinstance(p, float)]
277
+ ints = [p for p in params if isinstance(p, int) and not isinstance(p, bool)]
278
+ result["value_f"] = floats[0] if floats else None
279
+ result["value_i"] = ints[0] if ints else None
280
+
281
+ return result
282
+
283
+
284
+ def walk (
285
+ host: str,
286
+ address: str = "/",
287
+ port: int = WING_PORT,
288
+ timeout: float = 2.0,
289
+ max_depth: int = 3,
290
+ _depth: int = 0,
291
+ ) -> typing.Optional[typing.Dict[str, typing.Any]]:
292
+ """Recursively walk the WING's node tree from *address*.
293
+
294
+ Returns a nested dict representing the subtree. Stops at *max_depth*
295
+ levels to avoid issuing hundreds of queries against a large mixer tree.
296
+
297
+ Parameters:
298
+ host: WING IP address.
299
+ address: Starting node address (default ``"/"`` — the root).
300
+ port: UDP port (default 2223).
301
+ timeout: Per-query timeout.
302
+ max_depth: Maximum recursion depth (default 3).
303
+
304
+ Returns:
305
+ Nested ``dict`` — same structure as :func:`query` but with an extra
306
+ ``"children"`` key on nodes mapping child names to their own subtrees.
307
+ ``None`` on timeout.
308
+
309
+ Example::
310
+
311
+ tree = wing.walk(ip, "/ch/1", max_depth=1)
312
+ for name, child in tree["children"].items():
313
+ print(name, child)
314
+ """
315
+ result = query(host, address, port=port, timeout=timeout)
316
+ if result is None:
317
+ return None
318
+
319
+ if result["type"] == "node" and _depth < max_depth:
320
+ children_map: typing.Dict[str, typing.Any] = {}
321
+ for child_name in result["children"]:
322
+ child_address = address.rstrip("/") + "/" + child_name
323
+ child_result = walk(
324
+ host, child_address, port=port, timeout=timeout,
325
+ max_depth=max_depth, _depth=_depth + 1
326
+ )
327
+ children_map[child_name] = child_result
328
+ result["children"] = children_map
329
+
330
+ return result
331
+
332
+
333
+ def print_node (
334
+ host: str,
335
+ address: str = "/",
336
+ port: int = WING_PORT,
337
+ timeout: float = 2.0,
338
+ ) -> None:
339
+ """Query *address* and print a human-readable summary.
340
+
341
+ For nodes: prints the list of children.
342
+ For leaves: prints the current value(s).
343
+
344
+ Parameters:
345
+ host: WING IP address.
346
+ address: OSC address to query (default ``"/"``).
347
+ port: UDP port (default 2223).
348
+ timeout: Seconds to wait for a reply.
349
+
350
+ Example::
351
+
352
+ wing.print_node("192.168.0.116", "/ch/1")
353
+ wing.print_node("192.168.0.116", "/ch/1/fdr")
354
+ """
355
+ result = query(host, address, port=port, timeout=timeout)
356
+
357
+ if result is None:
358
+ print(f"{address} (no reply)")
359
+ return
360
+
361
+ if result["type"] == "node":
362
+ children = result["children"]
363
+ print(f"{address} [{len(children)} children]")
364
+ for child in children:
365
+ print(f" {child}")
366
+ else:
367
+ value_f = result.get("value_f")
368
+ value_i = result.get("value_i")
369
+ value_str = result.get("value", "")
370
+
371
+ parts: typing.List[str] = [repr(value_str)]
372
+ if value_f is not None:
373
+ parts.append(f"float={value_f}")
374
+ if value_i is not None:
375
+ parts.append(f"int={value_i}")
376
+
377
+ print(f"{address} {', '.join(parts)}")
378
+
379
+
380
+ # ── CLI entry point ───────────────────────────────────────────────────────────
381
+
382
+
383
+ def _main () -> None:
384
+ """Command-line interface.
385
+
386
+ Usage::
387
+
388
+ # Auto-discover WING on the LAN
389
+ python -m subsequence.helpers.wing
390
+
391
+ # Query a specific address (requires --host or auto-discovery)
392
+ python -m subsequence.helpers.wing /ch/1
393
+ python -m subsequence.helpers.wing /ch/1/fdr
394
+ python -m subsequence.helpers.wing --host 192.168.0.116 /ch/1
395
+ """
396
+ args = sys.argv[1:]
397
+
398
+ host: typing.Optional[str] = None
399
+ address: typing.Optional[str] = None
400
+
401
+ i = 0
402
+ while i < len(args):
403
+ if args[i] == "--host" and i + 1 < len(args):
404
+ host = args[i + 1]
405
+ i += 2
406
+ elif args[i].startswith("/"):
407
+ address = args[i]
408
+ i += 1
409
+ else:
410
+ i += 1
411
+
412
+ # Discover if no host given
413
+ if host is None:
414
+ print("Discovering WING on local network...")
415
+ device = discover()
416
+ if device is None:
417
+ print("No WING found.")
418
+ return
419
+ host = device["ip"]
420
+ print(
421
+ f"Found {device['device']} {device['model']} ({device['form_factor']}) "
422
+ f"at {host} firmware {device['firmware']}"
423
+ )
424
+
425
+ if address is not None:
426
+ print_node(host, address)
427
+
428
+
429
+ if __name__ == "__main__":
430
+ _main()