pydials 0.1.0b1__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.
pydials/__init__.py ADDED
@@ -0,0 +1,2455 @@
1
+ """pydials: dials, sliders, buttons, a piano keyboard, a scope and meters for a Pyctures scene.
2
+
3
+ from pyctures import *
4
+ from pydials import *
5
+
6
+ speed = dial("speed", 0, 10) # a number to read: speed.value, or speed * dt
7
+ dial("cutoff", lead, "filter_cutoff") # the dial IS that setting of `lead`
8
+ keyboard(lead, octave=4) # piano keys: fingers, the mouse, the computer's keys
9
+ scope(lead) # the wave `lead` is making, live
10
+
11
+ with group("filter"): # a titled plate that keeps its controls together
12
+ dial("cutoff", lead, "cutoff")
13
+ dial("resonance", lead, "resonance")
14
+
15
+ NOTHING IS POSITIONED BY HAND unless you want it. Controls line up in the order
16
+ they are made, on plates that share a width, and what does not fit can be moved
17
+ with a finger. ``group``, ``row``, ``column``, ``grid`` and ``page`` say what goes
18
+ with what; the page decides where each thing is, so Python is asked nothing.
19
+
20
+ THE PAGE DRAWS THE CONTROLS AND FEELS THE FINGERS. Python never touches the
21
+ page (it may be in a worker, or on another computer), so a dial that Python
22
+ drew and turned would lag behind the finger. Here each function sends ONE
23
+ command that makes the control in the page, from ``web/dials.js`` (the very
24
+ file that is the JavaScript library; a Pyctures that has it built in says so
25
+ with ``H_DIALS``, and then this module is one file that reads no other), and Python keeps a small object that
26
+ stands for it. What a hand changes comes back with the next frame: the object's
27
+ ``value`` is then up to date, and ``on_change`` is called.
28
+
29
+ A TARGET is anything a control can set, known only by what it can do:
30
+
31
+ set(name, value), get(name) else the control reads and writes target.name (or target[name])
32
+ describe(name) {"min", "max", "default", "unit", "step", "kind", "choices"}
33
+ note_on(note, velocity), note_off(note) what a keyboard plays
34
+ samples() its latest output, numbers from -1 to 1: a scope draws it
35
+ level() how loud it is now, 0 to 1: a meter shows it
36
+
37
+ A target that LIVES IN THE PAGE (a synth does: its sound is made there) says so
38
+ with a ``page`` attribute holding its Pyctures reference, or is such a
39
+ reference itself. Its controls are then tied to it in the page: a key sounds
40
+ the moment it is touched and a dial is heard as it turns, with no trip through
41
+ Python. Any other target is a Python object, and is told a frame later.
42
+
43
+ MIT licence.
44
+ """
45
+ import math
46
+ import operator
47
+ import os
48
+ import sys
49
+
50
+ __version__ = "0.0.0"
51
+
52
+ __all__ = ["dial", "slider", "button", "toggle", "select", "rotary", "keyboard", "scope", "meter",
53
+ "label", "group", "row", "column", "grid", "page", "spacer", "wrap", "layout", "room", "theme"]
54
+
55
+ _HERE = os.path.dirname(os.path.abspath(__file__))
56
+ _source = [None]
57
+
58
+ # How many points of a scope's line Python sends each frame, at most.
59
+ SCOPE_POINTS = 256
60
+
61
+
62
+ def _library():
63
+ """The page's half as text that can be made into a function: a page is given no file to load."""
64
+ if _source[0] is None:
65
+ with open(os.path.join(_HERE, "web", "dials.js"), encoding="utf-8") as f:
66
+ _source[0] = f.read().replace("export {", "return {")
67
+ return _source[0]
68
+
69
+
70
+ class _Run:
71
+ """One run of a scene: its page, and the controls made in it."""
72
+
73
+ def __init__(self, js=None):
74
+ self.js = js
75
+ self.lib = None
76
+ self.api = None
77
+ self.items = {}
78
+ self.next_id = 1
79
+ self.beating = False
80
+ self.room = None # the rectangle no control is on, as the page last told it
81
+ self.view = None # the picture's width and height and the scale the controls are drawn at, likewise
82
+ self.on_resize = None # what the scene asked to be called with them when they change
83
+
84
+
85
+ _run = _Run()
86
+
87
+
88
+ def _pyctures():
89
+ # Looked up each time: in a browser every scene has its own copy of the module.
90
+ module = sys.modules.get("pyctures")
91
+ if module is None:
92
+ import pyctures as module
93
+ return module
94
+
95
+
96
+ def _page():
97
+ """The page's controls for the scene that is running, made the first time one is wanted."""
98
+ global _run
99
+ p = _pyctures()
100
+ # A new run has a new `js`: what was made for the last run is gone with its page.
101
+ if _run.js is not p.js:
102
+ _run = _Run(p.js)
103
+ handle = getattr(p, "H_DIALS", None) or getattr(p, "H_CONTROLS", None)
104
+ if handle is not None:
105
+ _run.api = p.JsRef(handle) # a Pyctures that has the controls built in
106
+ else:
107
+ _run.lib = p.js.Function(_library())()
108
+ _run.api = _run.lib.dials(p.canvas, {"fresh": True})
109
+ _run.api.listen(_heard)
110
+ return _run.api
111
+
112
+
113
+ def _beat():
114
+ """Ask the page to call back every frame: something here must be looked at each frame."""
115
+ api = _page()
116
+ if not _run.beating:
117
+ _run.beating = True
118
+ api.beat()
119
+
120
+
121
+ def _heard(e):
122
+ """What the page reports, with a frame: what it made, what a hand changed, what was played."""
123
+ items = _run.items
124
+ made = e.get("m") or ()
125
+ for i in range(0, len(made) - 5, 6):
126
+ item = items.get(made[i])
127
+ if item is not None:
128
+ item._made(*made[i + 1:i + 6])
129
+ told = e.get("c") or ()
130
+ for i in range(0, len(told) - 1, 2):
131
+ item = items.get(told[i])
132
+ if item is not None:
133
+ item._told(told[i + 1])
134
+ played = e.get("n") or ()
135
+ for i in range(0, len(played) - 2, 3):
136
+ item = items.get(played[i])
137
+ if item is not None:
138
+ item._played(played[i + 1], played[i + 2])
139
+ if e.get("f"):
140
+ said = list(e["f"])
141
+ _run.room = said[:4]
142
+ if len(said) >= 7 and said[4:7] != _run.view:
143
+ _run.view = said[4:7]
144
+ if _run.on_resize:
145
+ _run.on_resize(Layout(*_run.view))
146
+ if e.get("b"):
147
+ for item in list(items.values()):
148
+ item._frame()
149
+ _run.api.ack()
150
+
151
+
152
+ # ---------------------------------------------------------------- targets
153
+
154
+ def _in_page(target):
155
+ """The target as an object in the page, when that is where it lives; else None."""
156
+ if target is None:
157
+ return None
158
+ if hasattr(type(target), "_enc"):
159
+ return target
160
+ page = getattr(target, "page", None)
161
+ return page if hasattr(type(page), "_enc") else None
162
+
163
+
164
+ def _describe(target, name):
165
+ ask = getattr(target, "describe", None)
166
+ d = ask(name) if callable(ask) else None
167
+ if d is None:
168
+ params = getattr(target, "params", None)
169
+ if isinstance(params, dict):
170
+ d = params.get(name)
171
+ d = dict(d) if isinstance(d, dict) else {}
172
+ if "range" in d:
173
+ d["min"], d["max"] = d["range"]
174
+ if isinstance(d.get("choices"), str):
175
+ d["choices"] = d["choices"].split("|") if d["choices"] else None
176
+ if d.get("kind") == "bool":
177
+ d["min"], d["max"] = 0, 1
178
+ return d
179
+
180
+
181
+ def _read(target, name):
182
+ if isinstance(target, dict):
183
+ value = target.get(name)
184
+ else:
185
+ get = getattr(target, "get", None)
186
+ value = get(name) if callable(get) else getattr(target, name, None)
187
+ return None if callable(value) else value
188
+
189
+
190
+ def _write(target, name, value):
191
+ if isinstance(target, dict):
192
+ target[name] = value
193
+ return
194
+ put = getattr(target, "set", None)
195
+ if callable(put):
196
+ put(name, value)
197
+ else:
198
+ setattr(target, name, value)
199
+
200
+
201
+ def _is_number(v):
202
+ return isinstance(v, (int, float)) and not isinstance(v, bool)
203
+
204
+
205
+ def _method(thing, *names):
206
+ for name in names:
207
+ fn = getattr(thing, name, None)
208
+ if callable(fn):
209
+ return fn
210
+ return None
211
+
212
+
213
+ # ---------------------------------------------------------------- controls
214
+
215
+ class Control:
216
+ """What Python keeps of a control that lives in the page."""
217
+ kind = ""
218
+
219
+ def __init__(self, spec):
220
+ api = _page()
221
+ self._id = _run.next_id
222
+ _run.next_id += 1
223
+ _run.items[self._id] = self
224
+ self.label = spec.get("label") or ""
225
+ self.on_change = spec.pop("on_change", None)
226
+ # (A pattern tied to something in the page is made with that thing's settings beside it, on a plate.)
227
+ make = api.pattern if getattr(self, "_plated", False) and "target" in spec else api.make
228
+ make(self.kind, self._id, {k: v for k, v in spec.items() if v is not None})
229
+
230
+ def _set(self, name, value):
231
+ _page().set(self._id, name, value)
232
+
233
+ def _call(self, name, *args):
234
+ _page().call(self._id, name, *args)
235
+
236
+ # What the page reports for this control; each kind takes what it needs.
237
+ def _made(self, low, high, default, choices, value):
238
+ pass
239
+
240
+ def _told(self, value):
241
+ pass
242
+
243
+ def _played(self, note, velocity):
244
+ pass
245
+
246
+ def _frame(self):
247
+ pass
248
+
249
+ def remove(self):
250
+ """Take it off the scene."""
251
+ _run.items.pop(self._id, None)
252
+ _page().drop(self._id)
253
+
254
+ def place(self, x=None, y=None):
255
+ """Put it at x, y (its top left corner); with nothing, the layout places it again."""
256
+ self._call("place", x, y)
257
+
258
+ def __repr__(self):
259
+ return "<%s %s %r>" % (self.kind, self.label, getattr(self, "value", None))
260
+
261
+
262
+ class _Number:
263
+ """A control that can be used where its value could: speed * dt, circle(x, y, size), if mute: …"""
264
+
265
+ def tolist(self): # how Pyctures takes a value that is not plain: this one's number
266
+ return self.value
267
+
268
+ def __float__(self):
269
+ return float(self.value)
270
+
271
+ def __int__(self):
272
+ return int(self.value)
273
+
274
+ def __index__(self):
275
+ value = self.value
276
+ if isinstance(value, bool) or float(value) != int(value):
277
+ raise TypeError("%r is not a whole number here; use int(…) or give the control step=1" % (self,))
278
+ return int(value)
279
+
280
+ def __bool__(self):
281
+ return bool(self.value)
282
+
283
+ def __str__(self):
284
+ return str(self.value)
285
+
286
+ def __format__(self, how):
287
+ return format(self.value, how)
288
+
289
+ def __neg__(self):
290
+ return -self.value
291
+
292
+ def __pos__(self):
293
+ return +self.value
294
+
295
+ def __abs__(self):
296
+ return abs(self.value)
297
+
298
+ def __round__(self, places=None):
299
+ return round(self.value, places)
300
+
301
+ def __eq__(self, other):
302
+ return self.value == (other.value if isinstance(other, _Number) else other)
303
+
304
+ def __ne__(self, other):
305
+ return not self.__eq__(other)
306
+
307
+ __hash__ = object.__hash__
308
+
309
+
310
+ def _value_of(v):
311
+ return v.value if isinstance(v, _Number) else v
312
+
313
+
314
+ for _name in ("add", "sub", "mul", "truediv", "floordiv", "mod", "pow", "lt", "le", "gt", "ge"):
315
+ _op = getattr(operator, _name)
316
+ setattr(_Number, "__%s__" % _name, lambda self, other, _op=_op: _op(self.value, _value_of(other)))
317
+ if _name not in ("lt", "le", "gt", "ge"):
318
+ setattr(_Number, "__r%s__" % _name, lambda self, other, _op=_op: _op(_value_of(other), self.value))
319
+
320
+
321
+ class _Bound(Control):
322
+ """A control that may be tied to a setting of a target, in the page or in Python."""
323
+
324
+ def _tie(self, spec, target, param):
325
+ """Fill the spec from the target. Returns what the target says of the setting ({} for one in the page)."""
326
+ self._target = None
327
+ self._owner = None
328
+ self._param = param
329
+ self._out = None
330
+ if target is None:
331
+ return {}
332
+ page = _in_page(target)
333
+ if page is not None:
334
+ # Tied in the page: the page asks it for the range and tells us (`_made`).
335
+ spec["target"], spec["param"] = page, param
336
+ # The Python object that stands for it is told what a hand changed, so `lead.cutoff` reads what the dial says.
337
+ self._owner = target if target is not page else None
338
+ return {}
339
+ self._target = target
340
+ d = _describe(target, param)
341
+ if not spec.get("label"):
342
+ spec["label"] = d.get("label") or d.get("name") or str(param)
343
+ _beat() # a Python target is looked at each frame: changed some other way, it moves the control
344
+ return d
345
+
346
+ def _send(self, value):
347
+ if self._target is not None:
348
+ _write(self._target, self._param, self._out(value) if self._out else value)
349
+ elif self._owner is not None:
350
+ hear = getattr(self._owner, "_told", None)
351
+ if callable(hear):
352
+ hear(self._param, value) # the page has already made the change: this only remembers it
353
+
354
+
355
+ class _Ranged(_Number, _Bound):
356
+ def __init__(self, label, a, b, c, spec):
357
+ if label is not None and not isinstance(label, str):
358
+ label, a, b, c = None, label, a, b
359
+ spec["label"] = label
360
+ target = None
361
+ if _is_number(a) or _is_number(b):
362
+ spec.setdefault("min", a)
363
+ spec.setdefault("max", b)
364
+ if c is not None:
365
+ spec.setdefault("value", c)
366
+ elif a is not None:
367
+ if not isinstance(b, str):
368
+ raise TypeError('%s("%s", …) takes a lowest and a highest number, or a target and the name of its setting: %s("cutoff", lead, "filter_cutoff")'
369
+ % (self.kind, label or "", self.kind))
370
+ target = a
371
+ d = self._tie(spec, target, b if target is not None else None)
372
+ for key in ("min", "max", "step", "unit", "log"):
373
+ if spec.get(key) is None and d.get(key) is not None:
374
+ spec[key] = d[key]
375
+ # A value with a name of its own (0 that means "auto"), and where a dial turned by ratio from 0 starts.
376
+ labels = spec.get("labels") or d.get("labels")
377
+ if labels:
378
+ spec["labels"] = {str(at): str(name) for at, name in labels.items()}
379
+ if spec.get("logFrom") is None and d.get("log_from") is not None:
380
+ spec["logFrom"] = d["log_from"]
381
+ if spec.get("step") is None and d.get("kind") == "int":
382
+ spec["step"] = 1
383
+ self._steps(spec)
384
+ self.min = spec.get("min", 0) if spec.get("min") is not None else 0
385
+ self.max = spec.get("max", 1) if spec.get("max") is not None else 1
386
+ self.step = spec.get("step") or 0
387
+ self.unit = spec.get("unit") or ""
388
+ now = spec.get("value")
389
+ given = now is not None
390
+ if now is None and self._target is not None:
391
+ now = _read(self._target, self._param)
392
+ if not _is_number(now):
393
+ now = None
394
+ self.default = spec.get("default")
395
+ if self.default is None:
396
+ self.default = d.get("default")
397
+ if "target" in spec:
398
+ # Tied in the page: its range and where it stands are told to us a frame from now (`_made`).
399
+ self._v = now if now is not None else 0.0
400
+ else:
401
+ if self.default is None:
402
+ self.default = now if now is not None else self.min
403
+ spec["default"] = self.default
404
+ self._v = self._clean(now if now is not None else self.default)
405
+ spec["value"] = self._v
406
+ if given:
407
+ self._send(self._v)
408
+ Control.__init__(self, spec)
409
+
410
+ def _steps(self, spec):
411
+ """A kind that always moves in steps says here what its step is when it was told none."""
412
+
413
+ def _clean(self, value):
414
+ low, high = min(self.min, self.max), max(self.min, self.max)
415
+ value = min(max(value, low), high)
416
+ if self.step:
417
+ places = max(0, -int(math.floor(math.log10(self.step)))) if self.step < 1 else 0
418
+ value = round(self.min + round((value - self.min) / self.step) * self.step, places)
419
+ if not places and isinstance(self.step, int) and isinstance(self.min, int):
420
+ value = int(value)
421
+ return value
422
+
423
+ @property
424
+ def value(self):
425
+ return self._v
426
+
427
+ @value.setter
428
+ def value(self, value):
429
+ value = self._clean(_value_of(value))
430
+ if value == self._v:
431
+ return
432
+ self._v = value
433
+ self._set("value", value)
434
+ self._send(value)
435
+
436
+ def reset(self):
437
+ """Back to where it started."""
438
+ self.value = self.default
439
+
440
+ def _made(self, low, high, default, choices, value):
441
+ self.min, self.max, self.default, self._v = low, high, default, value
442
+
443
+ def _told(self, value):
444
+ self._v = value
445
+ self._send(value)
446
+ if self.on_change:
447
+ self.on_change(value)
448
+
449
+ def _frame(self):
450
+ if self._target is None:
451
+ return
452
+ now = _read(self._target, self._param)
453
+ if _is_number(now) and abs(now - self._v) > abs(self.max - self.min) * 1e-6:
454
+ self._v = min(max(now, min(self.min, self.max)), max(self.min, self.max))
455
+ self._set("value", self._v)
456
+
457
+
458
+ class Dial(_Ranged):
459
+ kind = "dial"
460
+
461
+
462
+ class Slider(_Ranged):
463
+ kind = "slider"
464
+
465
+
466
+ class Button(Control):
467
+ kind = "button"
468
+
469
+ def __init__(self, spec):
470
+ self.on_press = spec.pop("on_press", None)
471
+ self.on_release = spec.pop("on_release", None)
472
+ self.held = False
473
+ self._presses = 0
474
+ Control.__init__(self, spec)
475
+
476
+ @property
477
+ def value(self):
478
+ return self.held
479
+
480
+ @property
481
+ def pressed(self):
482
+ """Was it pressed since this was last asked? `if fire.pressed:` in update() acts once for each press."""
483
+ if not self._presses:
484
+ return False
485
+ self._presses = 0
486
+ return True
487
+
488
+ def _told(self, down):
489
+ self.held = bool(down)
490
+ if down:
491
+ self._presses += 1
492
+ if self.on_press:
493
+ self.on_press()
494
+ elif self.on_release:
495
+ self.on_release()
496
+
497
+
498
+ class Toggle(_Number, _Bound):
499
+ kind = "toggle"
500
+
501
+ def __init__(self, label, a, b, spec):
502
+ spec["label"] = label
503
+ target = a if a is not None and isinstance(b, str) else None
504
+ if target is None and a is not None:
505
+ spec.setdefault("value", bool(a))
506
+ d = self._tie(spec, target, b if target is not None else None)
507
+ now = spec.get("value")
508
+ if self._target is not None:
509
+ there = _read(self._target, self._param)
510
+ # A target that keeps 0 and 1 is given 0 and 1.
511
+ if _is_number(there) or d.get("kind") == "bool":
512
+ self._out = lambda on: 1 if on else 0
513
+ if now is None:
514
+ now = there if there is not None else d.get("default")
515
+ self._v = bool(now)
516
+ if "target" not in spec or now is not None:
517
+ spec["value"] = self._v
518
+ Control.__init__(self, spec)
519
+
520
+ @property
521
+ def value(self):
522
+ return self._v
523
+
524
+ @value.setter
525
+ def value(self, on):
526
+ on = bool(_value_of(on))
527
+ if on == self._v:
528
+ return
529
+ self._v = on
530
+ self._set("value", on)
531
+ self._send(on)
532
+
533
+ on = value
534
+
535
+ def _made(self, low, high, default, choices, value):
536
+ self._v = bool(value)
537
+
538
+ def _told(self, value):
539
+ self._v = bool(value)
540
+ self._send(self._v)
541
+ if self.on_change:
542
+ self.on_change(self._v)
543
+
544
+ def _frame(self):
545
+ if self._target is None:
546
+ return
547
+ there = bool(_read(self._target, self._param))
548
+ if there != self._v:
549
+ self._v = there
550
+ self._set("value", there)
551
+
552
+
553
+ class Select(_Bound):
554
+ """One of a list. `value` is the choice, `index` its place in the list."""
555
+ kind = "select"
556
+
557
+ def __init__(self, label, a, b, spec):
558
+ spec["label"] = label
559
+ target = None
560
+ if isinstance(a, (list, tuple)):
561
+ spec["choices"] = list(a)
562
+ if b is not None:
563
+ spec.setdefault("value", b)
564
+ elif a is not None and isinstance(b, str):
565
+ target = a
566
+ d = self._tie(spec, target, b if target is not None else None)
567
+ if spec.get("choices") is None and d.get("choices"):
568
+ spec["choices"] = list(d["choices"])
569
+ self.choices = list(spec.get("choices") or [])
570
+ now = spec.get("value")
571
+ if self._target is not None:
572
+ there = _read(self._target, self._param)
573
+ # A target that keeps the choice as its number in the list is given the number.
574
+ by_number = _is_number(there) or d.get("kind") == "choice"
575
+ if by_number:
576
+ self._out = lambda choice: self._i
577
+ if now is None:
578
+ now = there if there is not None else d.get("default")
579
+ if by_number and _is_number(now):
580
+ now = self.choices[int(now)] if 0 <= int(now) < len(self.choices) else None
581
+ self._i = self.choices.index(now) if now in self.choices else 0
582
+ if self.choices:
583
+ spec["value"] = self.choices[self._i]
584
+ Control.__init__(self, spec)
585
+
586
+ @property
587
+ def value(self):
588
+ return self.choices[self._i] if self.choices else None
589
+
590
+ @value.setter
591
+ def value(self, choice):
592
+ if choice in self.choices:
593
+ self.index = self.choices.index(choice)
594
+
595
+ @property
596
+ def index(self):
597
+ return self._i
598
+
599
+ @index.setter
600
+ def index(self, i):
601
+ if not 0 <= i < len(self.choices) or i == self._i:
602
+ return
603
+ self._i = i
604
+ self._set("index", i)
605
+ self._send(self.choices[i])
606
+
607
+ def _made(self, low, high, default, choices, value):
608
+ self.choices = choices.split("|") if choices else []
609
+ self._i = int(value) if _is_number(value) else 0
610
+
611
+ def _told(self, i):
612
+ self._i = int(i)
613
+ self._send(self.value)
614
+ if self.on_change:
615
+ self.on_change(self.value)
616
+
617
+ def _frame(self):
618
+ if self._target is None:
619
+ return
620
+ # Changed some other way: by its place in the list, by its name in any case, or (of two) as False and True.
621
+ there = _read(self._target, self._param)
622
+ if isinstance(there, bool):
623
+ i = int(there) if len(self.choices) == 2 else None
624
+ elif there in self.choices:
625
+ i = self.choices.index(there)
626
+ elif _is_number(there):
627
+ i = int(round(there))
628
+ elif isinstance(there, str):
629
+ low = [str(choice).lower() for choice in self.choices]
630
+ i = low.index(there.lower()) if there.lower() in low else None
631
+ else:
632
+ i = None
633
+ if i is not None and 0 <= i < len(self.choices) and i != self._i:
634
+ self._i = i
635
+ self._set("index", self._i)
636
+
637
+ def __eq__(self, other):
638
+ return self.value == (other.value if isinstance(other, Select) else other)
639
+
640
+ __hash__ = object.__hash__
641
+
642
+ def __str__(self):
643
+ return str(self.value)
644
+
645
+
646
+ class Rotary(Select):
647
+ """A knob that clicks between a few choices. `value` is the choice, `index` its place in the list."""
648
+ kind = "rotary"
649
+
650
+
651
+ class Keyboard(Control):
652
+ """The piano keyboard. `notes` is what is held on it now (MIDI numbers: 60 is middle C)."""
653
+ kind = "keyboard"
654
+
655
+ def __init__(self, target, spec):
656
+ self.on_note_on = spec.pop("on_note_on", None)
657
+ self.on_note_off = spec.pop("on_note_off", None)
658
+ self._octave = spec.get("octave", 4) if spec.get("octave") is not None else 4
659
+ self._held = set()
660
+ page = _in_page(target)
661
+ self._target = None if page is not None else target
662
+ spec["target"] = page
663
+ Control.__init__(self, spec)
664
+
665
+ @property
666
+ def notes(self):
667
+ return sorted(self._held)
668
+
669
+ @property
670
+ def octave(self):
671
+ return self._octave
672
+
673
+ @octave.setter
674
+ def octave(self, n):
675
+ self._octave = max(0, min(8, int(n)))
676
+ self._set("octave", self._octave)
677
+
678
+ value = octave
679
+
680
+ def press(self, note, velocity=0.8):
681
+ """Play a note as a finger would."""
682
+ self._call("press", note, velocity)
683
+
684
+ def release(self, note=None):
685
+ """Let a note go; with no note, every note that is held."""
686
+ self._call("release", note)
687
+
688
+ def _told(self, octave):
689
+ self._octave = int(octave)
690
+
691
+ def _played(self, note, velocity):
692
+ if velocity:
693
+ self._held.add(note)
694
+ play = _method(self._target, "note_on", "noteOn")
695
+ if play:
696
+ play(note, velocity)
697
+ if self.on_note_on:
698
+ self.on_note_on(note, velocity)
699
+ else:
700
+ self._held.discard(note)
701
+ stop = _method(self._target, "note_off", "noteOff")
702
+ if stop:
703
+ stop(note)
704
+ if self.on_note_off:
705
+ self.on_note_off(note)
706
+
707
+
708
+ def _short(values):
709
+ """A line of numbers as the page is sent it: no more points than SCOPE_POINTS, three decimal places."""
710
+ values = getattr(values, "tolist", lambda: values)()
711
+ n = len(values)
712
+ stride = max(1, -(-n // SCOPE_POINTS))
713
+ return [round(float(values[i]), 3) for i in range(0, n, stride)]
714
+
715
+
716
+ class Scope(Control):
717
+ """A line of numbers: a sound's wave, or any value over time (`graph.push(value)`)."""
718
+ kind = "scope"
719
+
720
+ def __init__(self, source, spec):
721
+ page = _in_page(source)
722
+ self._pull = None
723
+ self._list = None
724
+ if page is not None:
725
+ spec["source"] = page
726
+ elif source is not None:
727
+ self._pull = source if callable(source) else _method(source, "samples", "scope")
728
+ if self._pull is None:
729
+ self._list = source
730
+ # A wave: held still, and drawn from -1 to 1, as one from the page is.
731
+ spec.setdefault("steady", True)
732
+ spec.setdefault("min", -1)
733
+ spec.setdefault("max", 1)
734
+ _beat()
735
+ self.value = None
736
+ Control.__init__(self, spec)
737
+
738
+ def push(self, value):
739
+ """One more point on the right; the oldest drops off the left."""
740
+ self.value = float(_value_of(value))
741
+ self._call("push", self.value)
742
+
743
+ def _frame(self):
744
+ if self._pull is None and self._list is None:
745
+ return
746
+ values = self._pull() if self._pull else self._list
747
+ if values is not None and len(values):
748
+ self._call("feed", _short(values))
749
+
750
+
751
+ class Meter(Control):
752
+ """A level from 0 to 1 (or a left and a right) as a bar of lights."""
753
+ kind = "meter"
754
+
755
+ def __init__(self, source, spec):
756
+ page = _in_page(source)
757
+ self._pull = None
758
+ if page is not None:
759
+ spec["source"] = page
760
+ elif source is not None:
761
+ self._pull = source if callable(source) else _method(source, "level")
762
+ _beat()
763
+ self._v = 0.0
764
+ Control.__init__(self, spec)
765
+
766
+ @property
767
+ def value(self):
768
+ return self._v
769
+
770
+ @value.setter
771
+ def value(self, level):
772
+ level = _value_of(level)
773
+ self._v = list(level) if isinstance(level, (list, tuple)) else float(level)
774
+ self._set("value", self._v)
775
+
776
+ def _frame(self):
777
+ if self._pull:
778
+ self.value = self._pull()
779
+
780
+
781
+ class Label(Control):
782
+ kind = "label"
783
+
784
+ @property
785
+ def value(self):
786
+ return self.label
787
+
788
+ @value.setter
789
+ def value(self, text):
790
+ self.label = str(text)
791
+ self._set("value", self.label)
792
+
793
+
794
+ class Box:
795
+ """A group, row, column, grid or page. Used with `with`, what is made inside is in it:
796
+
797
+ with group("filter"):
798
+ dial("cutoff", lead, "cutoff")
799
+
800
+ Used alone (`group("filter")`), what is made next is in it, until the next group, row or page.
801
+ """
802
+
803
+ def __init__(self, kind, spec):
804
+ api = _page()
805
+ self.kind = kind
806
+ self._id = _run.next_id
807
+ _run.next_id += 1
808
+ self.label = spec.get("label") or ""
809
+ api.make(kind, self._id, spec)
810
+
811
+ def __enter__(self):
812
+ _page().call(self._id, "enter")
813
+ return self
814
+
815
+ def __exit__(self, *error):
816
+ _page().call(self._id, "leave")
817
+ return False
818
+
819
+ @property
820
+ def title(self):
821
+ return self.label
822
+
823
+ @title.setter
824
+ def title(self, text):
825
+ self.label = str(text)
826
+ _page().set(self._id, "label", self.label)
827
+
828
+ def show(self):
829
+ """Turn to this page."""
830
+ _page().call(self._id, "show")
831
+
832
+ def remove(self):
833
+ """Take it, and everything in it, off the scene."""
834
+ _page().drop(self._id)
835
+
836
+ def __repr__(self):
837
+ return "<%s %s>" % (self.kind, self.label)
838
+
839
+
840
+ class Layout:
841
+ """What the layout came to: the picture's width and height, and the scale the controls are drawn at."""
842
+
843
+ def __init__(self, width, height, scale):
844
+ self.width, self.height, self.scale = width, height, scale
845
+
846
+ def __repr__(self):
847
+ return "Layout(width=%r, height=%r, scale=%r)" % (self.width, self.height, self.scale)
848
+
849
+
850
+ class Room:
851
+ """A rectangle: x, y, width and height, and its left, top, right and bottom edges."""
852
+
853
+ def __init__(self, x, y, width, height):
854
+ self.x, self.y, self.width, self.height = x, y, width, height
855
+
856
+ left = property(lambda self: self.x)
857
+ top = property(lambda self: self.y)
858
+ right = property(lambda self: self.x + self.width)
859
+ bottom = property(lambda self: self.y + self.height)
860
+
861
+ def __iter__(self):
862
+ return iter((self.x, self.y, self.width, self.height))
863
+
864
+ def __repr__(self):
865
+ return "Room(x=%r, y=%r, width=%r, height=%r)" % tuple(self)
866
+
867
+
868
+ # ---------------------------------------------------------------- what a scene calls
869
+
870
+ def _options(**given):
871
+ """The options as the page reads them: None is left out, and colour may be spelled color."""
872
+ if given.get("colour") is None:
873
+ given["colour"] = given.get("color")
874
+ given.pop("color", None)
875
+ return {name: value for name, value in given.items() if value is not None}
876
+
877
+
878
+ def dial(label=None, a=None, b=None, c=None, *, min=None, max=None, value=None, step=None, unit=None, log=None,
879
+ log_from=None, labels=None, digits=None, default=None, x=None, y=None, size=None, share=None, span=None, colour=None, color=None,
880
+ on_change=None):
881
+ """A dial: dial("speed", 0, 10), dial("speed", 0, 10, 5) or dial("cutoff", lead, "filter_cutoff").
882
+
883
+ size="small" or "large" (or how many points across); in a grid, span=2 covers two columns."""
884
+ return Dial(label, a, b, c, _options(min=min, max=max, value=value, step=step, unit=unit, log=log, logFrom=log_from,
885
+ labels=labels, digits=digits,
886
+ default=default, x=x, y=y, size=size, share=share, span=span, colour=colour,
887
+ color=color, on_change=on_change))
888
+
889
+
890
+ def slider(label=None, a=None, b=None, c=None, *, min=None, max=None, value=None, step=None, unit=None, log=None,
891
+ log_from=None, labels=None, digits=None, default=None, vertical=None, x=None, y=None, size=None, share=None, span=None, colour=None,
892
+ color=None, on_change=None):
893
+ """A slider, across or (vertical=True) upright: slider("volume", 0, 1) or slider("attack", lead, "attack").
894
+
895
+ share=1 gives it the room left over on its line."""
896
+ return Slider(label, a, b, c, _options(min=min, max=max, value=value, step=step, unit=unit, log=log, logFrom=log_from,
897
+ labels=labels, digits=digits,
898
+ default=default, vertical=vertical, x=x, y=y, size=size, share=share, span=span,
899
+ colour=colour, color=color, on_change=on_change))
900
+
901
+
902
+ def button(label="", on_press=None, *, on_release=None, x=None, y=None, width=None, size=None, share=None, span=None,
903
+ colour=None, color=None):
904
+ """A button: `fire = button("fire")`, then `if fire.pressed:` in update(); or button("fire", shoot)."""
905
+ return Button(_options(label=label, on_press=on_press, on_release=on_release, x=x, y=y, width=width, size=size,
906
+ share=share, span=span, colour=colour, color=color))
907
+
908
+
909
+ def toggle(label="", a=None, b=None, *, value=None, x=None, y=None, size=None, share=None, span=None, colour=None,
910
+ color=None, on_change=None):
911
+ """A switch: `mute = toggle("mute")`, then `if mute:`; toggle("mute", True); toggle("sync", lead, "sync")."""
912
+ return Toggle(label, a, b, _options(value=value, x=x, y=y, size=size, share=share, span=span, colour=colour, color=color,
913
+ on_change=on_change))
914
+
915
+
916
+ def select(label="", a=None, b=None, *, value=None, style=None, x=None, y=None, size=None, share=None, span=None,
917
+ colour=None, color=None, on_change=None):
918
+ """One of a list: select("wave", ["sine", "saw", "square"]), or select("wave", lead, "wave").
919
+
920
+ style="rotary" draws it as a knob that clicks between the choices: the same as rotary(…)."""
921
+ kind = Rotary if style == "rotary" else Select
922
+ return kind(label, a, b, _options(value=value, x=x, y=y, size=size, share=share, span=span, colour=colour, color=color,
923
+ on_change=on_change))
924
+
925
+
926
+ def rotary(label="", a=None, b=None, *, value=None, show_value=None, x=None, y=None, size=None, share=None, span=None,
927
+ colour=None, color=None, on_change=None):
928
+ """A knob that clicks between a few choices, as the one on a synth that picks a waveform does.
929
+
930
+ rotary("mode", ["room", "hall", "plate", "spring"], "hall"), or rotary("wave", lead, "wave") for a setting
931
+ that has choices. Drag to turn it from notch to notch, or tap a name; `mode.value` is the choice and
932
+ `mode.index` its place. The names are written round it where there is room: size="large" shows more. The
933
+ choice is written under the knob only where no names are round it; show_value=True or False says otherwise."""
934
+ return Rotary(label, a, b, _options(value=value, showValue=show_value, x=x, y=y, size=size, share=share, span=span, colour=colour, color=color,
935
+ on_change=on_change))
936
+
937
+
938
+ def keyboard(target=None, *, octave=None, octaves=None, keys=None, hints=None, animate=None, x=None, y=None, width=None,
939
+ height=None, share=None, span=None, colour=None, color=None, on_note_on=None, on_note_off=None):
940
+ """A piano keyboard that plays `target`: keyboard(lead, octave=4). With no target, read `notes` or give on_note_on.
941
+
942
+ Made outside any group it lies along the bottom; made in one, it is where the group puts it. The keys slide
943
+ when the octave changes; animate=False makes them jump."""
944
+ return Keyboard(target, _options(octave=octave, octaves=octaves, keys=keys, hints=hints, animate=animate, x=x, y=y, width=width,
945
+ height=height, share=share, span=span, colour=colour, color=color,
946
+ on_note_on=on_note_on, on_note_off=on_note_off))
947
+
948
+
949
+ def _labelled(label, source):
950
+ if isinstance(label, str):
951
+ return label, source
952
+ return (source if isinstance(source, str) else None), label
953
+
954
+
955
+ def scope(label=None, source=None, *, points=None, min=None, max=None, steady=None, x=None, y=None, width=None,
956
+ height=None, share=None, span=None, colour=None, color=None):
957
+ """A scope: scope(lead) draws its wave; `graph = scope("height")` then graph.push(value) draws a value over time.
958
+
959
+ Made outside any group it is as wide as the picture; in a group, share=1 gives it the room left over."""
960
+ label, source = _labelled(label, source)
961
+ return Scope(source, _options(label=label, points=points, min=min, max=max, steady=steady, x=x, y=y, width=width,
962
+ height=height, share=share, span=span, colour=colour, color=color))
963
+
964
+
965
+ def meter(label=None, source=None, *, vertical=None, x=None, y=None, size=None, share=None, span=None, colour=None,
966
+ color=None):
967
+ """A level meter: meter(lead), or `level = meter("level")` then level.value = 0.7."""
968
+ label, source = _labelled(label, source)
969
+ return Meter(source, _options(label=label, vertical=vertical, x=x, y=y, size=size, share=share, span=span,
970
+ colour=colour, color=color))
971
+
972
+
973
+ def label(text, *, x=None, y=None, span=None, colour=None, color=None):
974
+ """Words. Made just before some controls and outside any group, it is their heading: the title of their plate."""
975
+ return Label(_options(label=str(text), x=x, y=y, span=span, colour=colour, color=color))
976
+
977
+
978
+ def _box(kind, title, **options):
979
+ spec = _options(**options)
980
+ if title is not None:
981
+ spec["label"] = str(title)
982
+ return Box(kind, spec)
983
+
984
+
985
+ def group(title=None, *, columns=None, wrap=None, at=None, share=None, span=None, x=None, y=None, width=None, height=None,
986
+ plate=None, dividers=None, gap=None, align=None, colour=None, color=None):
987
+ """A titled plate that keeps its controls together.
988
+
989
+ with group("filter"): # what is made inside is in the group
990
+ dial("cutoff", lead, "cutoff")
991
+
992
+ group("filter") # or in a straight line: what is made next is in it,
993
+ dial("cutoff", lead, "cutoff") # until the next group, row or page
994
+
995
+ wrap=False keeps its controls on one line whatever the width; columns=3 lines them up three to a line; at="left" (or "right", "top", "bottom") makes it a strip along
996
+ that edge; share=2 gives it twice its neighbour's part of the line; colour= is the colour of its controls."""
997
+ return _box("group", title, columns=columns, wrap=wrap, at=at, share=share, span=span, x=x, y=y, width=width, height=height,
998
+ plate=plate, dividers=dividers, gap=gap, align=align, colour=colour, color=color)
999
+
1000
+
1001
+ def row(title=None, *, wrap=None, share=None, span=None, plate=None, dividers=None, gap=None, align=None, colour=None,
1002
+ color=None):
1003
+ """`with row():` what is made inside stands in a line. `row()` alone: the controls made after this start a new line.
1004
+
1005
+ `with row(wrap=False):` never splits: two groups made inside it stay side by side."""
1006
+ return _box("row", title, wrap=wrap, share=share, span=span, plate=plate, dividers=dividers, gap=gap, align=align,
1007
+ colour=colour, color=color)
1008
+
1009
+
1010
+ def column(title=None, *, share=None, span=None, plate=None, dividers=None, gap=None, align=None, colour=None, color=None):
1011
+ """`with column():` what is made inside stands one under another."""
1012
+ return _box("column", title, share=share, span=span, plate=plate, dividers=dividers, gap=gap, align=align,
1013
+ colour=colour, color=color)
1014
+
1015
+
1016
+ def grid(columns=None, title=None, *, share=None, span=None, plate=None, dividers=None, gap=None, align=None,
1017
+ colour=None, color=None):
1018
+ """`with grid(3):` what is made inside is three to a line, in columns that line up (fewer where three will not fit)."""
1019
+ if isinstance(columns, str):
1020
+ columns, title = (title if _is_number(title) else None), columns
1021
+ return _box("grid", title, columns=columns, share=share, span=span, plate=plate, dividers=dividers, gap=gap,
1022
+ align=align, colour=colour, color=color)
1023
+
1024
+
1025
+ def page(name=None, *, colour=None, color=None):
1026
+ """A page of plates, turned to with a tab: `with page("fx"):`, or page("fx") and then its controls.
1027
+
1028
+ What is made before the first page is above the tabs on every page. page() alone ends the pages: what is
1029
+ made next is below them on every page."""
1030
+ return _box("page", name, colour=colour, color=color)
1031
+
1032
+
1033
+ def wrap():
1034
+ """If this line has to be split, here is where. Nothing happens while everything fits."""
1035
+ api = _page()
1036
+ _run.next_id += 1
1037
+ api.make("row", _run.next_id - 1, {"soft": True})
1038
+
1039
+
1040
+ def spacer(size=None, *, share=None):
1041
+ """Room between controls: spacer(20) is that many points, spacer() all the room that is left over."""
1042
+ api = _page()
1043
+ _run.next_id += 1
1044
+ api.make("spacer", _run.next_id - 1, _options(size=size, share=share))
1045
+
1046
+
1047
+ def layout(**options):
1048
+ """How it is all laid out, each of these left out or given:
1049
+
1050
+ wrap=False every row stays one row; everything is drawn smaller until the widest fits the width
1051
+ wrap=True every row() is a new line, and a row is split only when it does not fit
1052
+ min_scale=0.8 told not to wrap, no smaller than this: wider rows are moved from side to side
1053
+ fit="width" as wrap=False, and drawn bigger too where there is room, no bigger than max_scale= (1.6)
1054
+ columns=3 so many plates to a line; or by width: columns={"phone": 1, "tablet": 2, "wide": 3}
1055
+ size=(800, 480) the panel is laid out at this size on every screen, then made to fit the picture whole;
1056
+ size=800 is a width alone (as is fit="width"): what does not fit is moved up and down
1057
+ scale=1.2 everything bigger or smaller
1058
+ on_resize=fn fn(now) is called when the picture changes size: now.width, now.height, now.scale
1059
+ at="bottom" (or "top", "left", "right"), margin=10, align="center", compact=True, dividers=True, plates=False,
1060
+ backdrop="navy", image="panel.png" (a picture behind the controls)
1061
+
1062
+ It gives back the picture's width and height and the scale the controls are drawn at, as the page last said
1063
+ them: `layout().width`. In a scene's first lines the page has said nothing yet, and the scale is 1."""
1064
+ api = _page()
1065
+ if "on_resize" in options:
1066
+ _run.on_resize = options.pop("on_resize")
1067
+ for name, theirs in (("min_scale", "minScale"), ("max_scale", "maxScale")):
1068
+ if name in options:
1069
+ options[theirs] = options.pop(name)
1070
+ for name in ("size", "design"):
1071
+ if isinstance(options.get(name), tuple):
1072
+ options[name] = list(options[name])
1073
+ if options:
1074
+ api.layout(options)
1075
+ if _run.view:
1076
+ return Layout(*_run.view)
1077
+ size = getattr(_pyctures(), "_scene_size", None)
1078
+ return Layout(*(size() if size else (0, 0)), 1)
1079
+
1080
+
1081
+ def room():
1082
+ """The rectangle of the picture that no control is on, for the scene to draw in: its x, y, width and height.
1083
+
1084
+ The page works it out and says so with a frame, so in the scene's first lines it is still the whole picture."""
1085
+ _page()
1086
+ if _run.room:
1087
+ return Room(*_run.room[:4])
1088
+ size = getattr(_pyctures(), "_scene_size", None)
1089
+ return Room(0, 0, *(size() if size else (0, 0)))
1090
+
1091
+
1092
+ def theme(**colours):
1093
+ """Change the colours: theme(accent="deepskyblue")."""
1094
+ _page().theme(colours)
1095
+
1096
+
1097
+ # ================================================================ more controls
1098
+ # Each kind below is made in the page by one command, as the ones above are. They are in the order of
1099
+ # the README: for any program, for games, for music.
1100
+
1101
+ __all__ += ["number", "stepper", "text_box", "readout", "checkbox", "choose", "radio", "dropdown", "xy_pad", "colour_picker",
1102
+ "color_picker", "progress", "light", "busy", "range_slider", "fader", "joystick", "dpad", "action_button",
1103
+ "envelope", "step_grid", "pads", "wheel", "mod_wheel", "pitch_wheel",
1104
+ "steps", "trance_gate", "arp_pattern", "drum_seq"]
1105
+
1106
+
1107
+ class Number(_Ranged):
1108
+ """A number with − and + at its sides, which can be typed."""
1109
+ kind = "number"
1110
+
1111
+ def _steps(self, spec):
1112
+ # Told no step, it counts in ones; a range too small for that, in tenths or hundredths. (Tied in the page, the page says.)
1113
+ if spec.get("step") is None and "target" not in spec and spec.get("min") is not None and spec.get("max") is not None:
1114
+ wide = abs(spec["max"] - spec["min"])
1115
+ spec["step"] = 1 if wide >= 5 else 0.1 if wide >= 0.5 else 0.01
1116
+
1117
+
1118
+ class TextBox(_Bound):
1119
+ """One line of text that is typed. `value` is the text."""
1120
+ kind = "textBox"
1121
+
1122
+ def __init__(self, label, a, b, spec):
1123
+ self.on_enter = spec.pop("on_enter", None)
1124
+ spec["label"] = label
1125
+ target = a if a is not None and not isinstance(a, (str, int, float)) and isinstance(b, str) else None
1126
+ if target is None and a is not None:
1127
+ spec.setdefault("value", str(a))
1128
+ self._tie(spec, target, b if target is not None else None)
1129
+ now = spec.get("value")
1130
+ if now is None and self._target is not None:
1131
+ now = _read(self._target, self._param)
1132
+ self._v = "" if now is None else str(now)
1133
+ if "target" not in spec:
1134
+ spec["value"] = self._v
1135
+ Control.__init__(self, spec)
1136
+
1137
+ @property
1138
+ def value(self):
1139
+ return self._v
1140
+
1141
+ @value.setter
1142
+ def value(self, text):
1143
+ text = str(text)
1144
+ if text == self._v:
1145
+ return
1146
+ self._v = text
1147
+ self._set("value", text)
1148
+ self._send(text)
1149
+
1150
+ def _made(self, low, high, default, choices, value):
1151
+ self._v = "" if value is None else str(value)
1152
+
1153
+ def _told(self, value):
1154
+ # Enter is told with the text in a list of its own.
1155
+ if isinstance(value, (list, tuple)):
1156
+ if self.on_enter:
1157
+ self.on_enter(self._v)
1158
+ return
1159
+ self._v = str(value)
1160
+ self._send(self._v)
1161
+ if self.on_change:
1162
+ self.on_change(self._v)
1163
+
1164
+ def _frame(self):
1165
+ if self._target is None:
1166
+ return
1167
+ there = _read(self._target, self._param)
1168
+ if there is not None and str(there) != self._v:
1169
+ self._v = str(there)
1170
+ self._set("value", self._v)
1171
+
1172
+ def __str__(self):
1173
+ return self._v
1174
+
1175
+ def __eq__(self, other):
1176
+ return self._v == (other.value if isinstance(other, TextBox) else other)
1177
+
1178
+ __hash__ = object.__hash__
1179
+
1180
+ def __len__(self):
1181
+ return len(self._v)
1182
+
1183
+
1184
+ class Readout(Control):
1185
+ """A value or a message shown: it takes no hand. `value` is what it shows."""
1186
+ kind = "readout"
1187
+ _asks = () # what a source is asked when no setting of it is named: a light asks its level()
1188
+
1189
+ def __init__(self, source, param, spec):
1190
+ self._pull = None
1191
+ self._format = spec.pop("format", None)
1192
+ if callable(self._format):
1193
+ self._show = self._format # Python makes the words, and the page shows them
1194
+ else:
1195
+ self._show = None
1196
+ if self._format is not None:
1197
+ spec["format"] = self._format
1198
+ page = _in_page(source) if isinstance(param, str) else None
1199
+ if page is not None:
1200
+ spec["target"], spec["param"] = page, param
1201
+ elif self._asks and _in_page(source) is not None:
1202
+ spec["source"] = _in_page(source)
1203
+ elif callable(source):
1204
+ self._pull = source
1205
+ elif self._asks and _method(source, *self._asks):
1206
+ self._pull = _method(source, *self._asks)
1207
+ elif source is not None and isinstance(param, str):
1208
+ self._pull = lambda: _read(source, param)
1209
+ d = _describe(source, param)
1210
+ for key in ("unit", "digits"):
1211
+ if spec.get(key) is None and d.get(key) is not None:
1212
+ spec[key] = d[key]
1213
+ if not spec.get("label"):
1214
+ spec["label"] = d.get("label") or d.get("name") or param
1215
+ elif source is not None:
1216
+ spec.setdefault("value", source)
1217
+ self._v = self._pull() if self._pull else spec.get("value", "")
1218
+ if self._pull:
1219
+ _beat()
1220
+ spec["value"] = self._sent = self._shown()
1221
+ Control.__init__(self, spec)
1222
+
1223
+ def _shown(self):
1224
+ v = _value_of(self._v)
1225
+ return str(self._show(v)) if self._show else v
1226
+
1227
+ @property
1228
+ def value(self):
1229
+ return self._v
1230
+
1231
+ @value.setter
1232
+ def value(self, value):
1233
+ self._v = value
1234
+ now = self._shown()
1235
+ if now != self._sent:
1236
+ self._sent = now
1237
+ self._set("value", now)
1238
+
1239
+ def _frame(self):
1240
+ if self._pull:
1241
+ self.value = self._pull()
1242
+
1243
+
1244
+ def number(label=None, a=None, b=None, c=None, *, min=None, max=None, value=None, step=None, unit=None, digits=None,
1245
+ default=None, x=None, y=None, share=None, span=None, colour=None, color=None, on_change=None):
1246
+ """A number with − and + at its sides: number("lives", 0, 9, 3), or number("voices", lead, "voices").
1247
+
1248
+ Tap − or + to step and hold one to keep stepping, drag up or down on the number, or tap it and type.
1249
+ It counts in ones unless step= says otherwise (a small range counts in tenths). `lives.value`, or `lives * 2`."""
1250
+ return Number(label, a, b, c, _options(min=min, max=max, value=value, step=step, unit=unit, digits=digits, default=default,
1251
+ x=x, y=y, share=share, span=span, colour=colour, color=color, on_change=on_change))
1252
+
1253
+
1254
+ stepper = number
1255
+
1256
+
1257
+ def text_box(label="", a=None, b=None, *, value=None, placeholder=None, max=None, width=None, x=None, y=None, share=None,
1258
+ span=None, colour=None, color=None, on_change=None, on_enter=None):
1259
+ """A line of text that is typed: `name = text_box("name", "Ada")`, then `name.value`.
1260
+
1261
+ text_box("name", player, "name") is that setting of `player`. placeholder= is shown while it is empty and
1262
+ max= is the most letters it takes; on_change(text) is called as each letter is typed (a frame later) and
1263
+ on_enter(text) when Enter is pressed."""
1264
+ return TextBox(label, a, b, _options(value=value, placeholder=placeholder, max=max, width=width, x=x, y=y, share=share,
1265
+ span=span, colour=colour, color=color, on_change=on_change, on_enter=on_enter))
1266
+
1267
+
1268
+ def readout(label="", source=None, setting=None, *, value=None, unit=None, digits=None, format=None, size=None, width=None,
1269
+ x=None, y=None, share=None, span=None, colour=None, color=None):
1270
+ """A value or a message shown: readout("score", lambda: score), or `score = readout("score")` and `score.value = 12`.
1271
+
1272
+ readout("cutoff", lead, "cutoff") shows a setting. size="large" is for a score; format="clock" shows seconds as
1273
+ minutes and seconds, and format= may be a function that makes the words; unit= and digits= are for a number."""
1274
+ return Readout(source, setting, _options(label=label, value=value, unit=unit, digits=digits, format=format, size=size,
1275
+ width=width, x=x, y=y, share=share, span=span, colour=colour, color=color))
1276
+
1277
+
1278
+ class Checkbox(Toggle):
1279
+ """A box that is ticked. `on` (or `value`) is True or False."""
1280
+ kind = "checkbox"
1281
+
1282
+
1283
+ class Choose(Select):
1284
+ """One of a few, side by side. `value` is the choice, `index` its place in the list."""
1285
+ kind = "choose"
1286
+
1287
+
1288
+ class Radio(Select):
1289
+ """One of a few, one under another. `value` is the choice, `index` its place in the list."""
1290
+ kind = "radio"
1291
+
1292
+
1293
+ class Dropdown(Select):
1294
+ """One of a long list, which opens over the controls. `value` is the choice, `index` its place in the list."""
1295
+ kind = "dropdown"
1296
+
1297
+
1298
+ def checkbox(label="", a=None, b=None, *, value=None, x=None, y=None, share=None, span=None, colour=None, color=None,
1299
+ on_change=None):
1300
+ """A box that is ticked: `sound = checkbox("sound", True)`, then `if sound:`; checkbox("sync", lead, "sync")."""
1301
+ return Checkbox(label, a, b, _options(value=value, x=x, y=y, share=share, span=span, colour=colour, color=color,
1302
+ on_change=on_change))
1303
+
1304
+
1305
+ def _chosen(kind, label, a, b, value, x, y, share, span, colour, color, on_change):
1306
+ return kind(label, a, b, _options(value=value, x=x, y=y, share=share, span=span, colour=colour, color=color,
1307
+ on_change=on_change))
1308
+
1309
+
1310
+ def choose(label="", a=None, b=None, *, value=None, x=None, y=None, share=None, span=None, colour=None, color=None,
1311
+ on_change=None):
1312
+ """One of a few, all in sight side by side: `mode = choose("mode", ["easy", "medium", "hard"], "medium")`.
1313
+
1314
+ `mode.value` is the choice and `mode.index` its place; choose("wave", lead, "wave") is a setting that has
1315
+ choices. Where they do not fit on one line they take two."""
1316
+ return _chosen(Choose, label, a, b, value, x, y, share, span, colour, color, on_change)
1317
+
1318
+
1319
+ def radio(label="", a=None, b=None, *, value=None, x=None, y=None, share=None, span=None, colour=None, color=None,
1320
+ on_change=None):
1321
+ """One of a few, one under another with a round mark each: radio("size", ["small", "medium", "large"])."""
1322
+ return _chosen(Radio, label, a, b, value, x, y, share, span, colour, color, on_change)
1323
+
1324
+
1325
+ def dropdown(label="", a=None, b=None, *, value=None, x=None, y=None, share=None, span=None, colour=None, color=None,
1326
+ on_change=None):
1327
+ """One of a long list: dropdown("planet", planets, "Earth"). A tap opens the list over the controls; it is
1328
+ moved with a finger or the wheel when it is long, and closes on a pick or a touch outside it."""
1329
+ return _chosen(Dropdown, label, a, b, value, x, y, share, span, colour, color, on_change)
1330
+
1331
+
1332
+ class XYPad(_Bound):
1333
+ """A square where a finger sets two numbers at once. `x` and `y` are the numbers, and `value` is both."""
1334
+ kind = "xyPad"
1335
+
1336
+ def __init__(self, label, a, b, c, spec):
1337
+ if label is not None and not isinstance(label, str):
1338
+ label, a, b, c = None, label, a, b
1339
+ spec["label"] = label
1340
+ self._params = None
1341
+ self._ways = [[0, 1], [0, 1]]
1342
+ target = None
1343
+ if isinstance(a, (list, tuple)):
1344
+ self._ways = [list(a), list(b if isinstance(b, (list, tuple)) else a)]
1345
+ spec["across"], spec["up"] = self._ways
1346
+ elif a is not None:
1347
+ if not (isinstance(b, str) and isinstance(c, str)):
1348
+ raise TypeError('xy_pad("%s", …) takes two ranges, or a target and the names of two of its settings: '
1349
+ 'xy_pad("filter", lead, "cutoff", "resonance")' % (label or ""))
1350
+ target = a
1351
+ self._tie(spec, target, b if target is not None else None)
1352
+ if target is not None:
1353
+ self._params = (b, c)
1354
+ if "target" in spec: # tied in the page, which says where it stands a frame from now
1355
+ spec["paramX"], spec["paramY"] = b, c
1356
+ del spec["param"]
1357
+ else:
1358
+ self._ways = []
1359
+ for name in (b, c):
1360
+ d = _describe(target, name)
1361
+ now = _read(target, name)
1362
+ self._ways.append([d.get("min", 0), d.get("max", 1)] + ([now] if _is_number(now) else []))
1363
+ spec["across"], spec["up"] = self._ways
1364
+ if not label:
1365
+ spec["label"] = "%s / %s" % (b, c)
1366
+ self._v = [way[2] if len(way) > 2 else (way[0] + way[1]) / 2 for way in self._ways]
1367
+ Control.__init__(self, spec)
1368
+
1369
+ def _clean(self, pair):
1370
+ return [min(max(_value_of(v), min(way[0], way[1])), max(way[0], way[1])) for v, way in zip(pair, self._ways)]
1371
+
1372
+ @property
1373
+ def value(self):
1374
+ return list(self._v)
1375
+
1376
+ @value.setter
1377
+ def value(self, pair):
1378
+ pair = self._clean(pair)
1379
+ if pair == self._v:
1380
+ return
1381
+ self._v = pair
1382
+ self._set("value", pair)
1383
+ self._both(pair)
1384
+
1385
+ x = property(lambda self: self._v[0], lambda self, v: setattr(self, "value", [v, self._v[1]]))
1386
+ y = property(lambda self: self._v[1], lambda self, v: setattr(self, "value", [self._v[0], v]))
1387
+
1388
+ def _both(self, pair):
1389
+ if self._target is not None:
1390
+ for name, value in zip(self._params, pair):
1391
+ _write(self._target, name, value)
1392
+ elif self._owner is not None and callable(getattr(self._owner, "_told", None)):
1393
+ for name, value in zip(self._params, pair):
1394
+ self._owner._told(name, value)
1395
+
1396
+ def _made(self, low, high, default, choices, value):
1397
+ if isinstance(value, (list, tuple)):
1398
+ self._v = list(value)
1399
+
1400
+ def _told(self, pair):
1401
+ self._v = list(pair)
1402
+ self._both(self._v)
1403
+ if self.on_change:
1404
+ self.on_change(self._v[0], self._v[1])
1405
+
1406
+ def _frame(self):
1407
+ if self._target is None:
1408
+ return
1409
+ there = [_read(self._target, name) for name in self._params]
1410
+ if all(_is_number(v) for v in there) and there != self._v:
1411
+ self._v = there
1412
+ self._set("value", there)
1413
+
1414
+ def __iter__(self):
1415
+ return iter(self._v)
1416
+
1417
+
1418
+ class ColourPicker(TextBox):
1419
+ """A patch of colour that opens a picker. `value` is the colour as a scene writes one: "tomato", "#3a7bd5"."""
1420
+ kind = "colourPicker"
1421
+
1422
+
1423
+ class Progress(Readout):
1424
+ """A bar that fills. `value` is how far it has got."""
1425
+ kind = "progress"
1426
+ _asks = ("progress", "level")
1427
+
1428
+
1429
+ class Light(Readout):
1430
+ """A light: on or off, a brightness from 0 to 1, or a colour."""
1431
+ kind = "light"
1432
+ _asks = ("level",)
1433
+
1434
+ @property
1435
+ def on(self):
1436
+ v = _value_of(self._v)
1437
+ if isinstance(v, (list, tuple)):
1438
+ v = max(v)
1439
+ return bool(v) and v != "off"
1440
+
1441
+ @on.setter
1442
+ def on(self, on):
1443
+ self.value = bool(_value_of(on))
1444
+
1445
+ def __bool__(self):
1446
+ return self.on
1447
+
1448
+
1449
+ class Busy(Light):
1450
+ """A ring that goes round while something is happening."""
1451
+ kind = "busy"
1452
+ _asks = ()
1453
+
1454
+
1455
+ class RangeSlider(_Bound):
1456
+ """A slider with two handles. `low` and `high` are the numbers, and `value` is both."""
1457
+ kind = "rangeSlider"
1458
+
1459
+ def __init__(self, label, a, b, c, spec):
1460
+ if label is not None and not isinstance(label, str):
1461
+ label, a, b, c = None, label, a, b
1462
+ spec["label"] = label
1463
+ self._params = None
1464
+ target = None
1465
+ if _is_number(a):
1466
+ spec.setdefault("min", a)
1467
+ spec.setdefault("max", b)
1468
+ if c is not None:
1469
+ spec.setdefault("value", list(c))
1470
+ elif a is not None:
1471
+ if not (isinstance(b, str) and isinstance(c, str)):
1472
+ raise TypeError('range_slider("%s", …) takes a lowest and a highest number, or a target and the names of two '
1473
+ 'of its settings: range_slider("loop", sample, "start", "end")' % (label or ""))
1474
+ target = a
1475
+ d = self._tie(spec, target, b if target is not None else None)
1476
+ if target is not None:
1477
+ self._params = (b, c)
1478
+ if "target" in spec:
1479
+ spec["paramHigh"] = c
1480
+ else:
1481
+ for key in ("min", "max", "step", "unit"):
1482
+ if spec.get(key) is None and d.get(key) is not None:
1483
+ spec[key] = d[key]
1484
+ if spec.get("value") is None:
1485
+ there = [_read(target, b), _read(target, c)]
1486
+ if all(_is_number(v) for v in there):
1487
+ spec["value"] = there
1488
+ self.min = spec.get("min", 0)
1489
+ self.max = spec.get("max", 1)
1490
+ self._v = self._clean(spec.get("value") or [self.min, self.max])
1491
+ if "target" not in spec:
1492
+ spec["value"] = self._v
1493
+ Control.__init__(self, spec)
1494
+
1495
+ def _clean(self, pair):
1496
+ low, high = min(self.min, self.max), max(self.min, self.max)
1497
+ pair = sorted(min(max(_value_of(v), low), high) for v in pair)
1498
+ return pair
1499
+
1500
+ @property
1501
+ def value(self):
1502
+ return list(self._v)
1503
+
1504
+ @value.setter
1505
+ def value(self, pair):
1506
+ pair = self._clean(pair)
1507
+ if pair == self._v:
1508
+ return
1509
+ self._v = pair
1510
+ self._set("value", pair)
1511
+ self._both(pair)
1512
+
1513
+ low = property(lambda self: self._v[0], lambda self, v: setattr(self, "value", [min(_value_of(v), self._v[1]), self._v[1]]))
1514
+ high = property(lambda self: self._v[1], lambda self, v: setattr(self, "value", [self._v[0], max(_value_of(v), self._v[0])]))
1515
+
1516
+ _both = XYPad._both
1517
+
1518
+ def _made(self, low, high, default, choices, value):
1519
+ self.min, self.max = low, high
1520
+ if isinstance(value, (list, tuple)):
1521
+ self._v = list(value)
1522
+
1523
+ def _told(self, pair):
1524
+ self._v = list(pair)
1525
+ self._both(self._v)
1526
+ if self.on_change:
1527
+ self.on_change(self._v[0], self._v[1])
1528
+
1529
+ _frame = XYPad._frame
1530
+
1531
+ def __iter__(self):
1532
+ return iter(self._v)
1533
+
1534
+
1535
+ class Fader(_Ranged):
1536
+ kind = "fader"
1537
+
1538
+
1539
+ def xy_pad(label=None, a=None, b=None, c=None, *, spring=None, x=None, y=None, size=None, share=None, span=None,
1540
+ colour=None, color=None, on_change=None):
1541
+ """A square where one finger sets two numbers at once, one by going across and one by going up.
1542
+
1543
+ xy_pad("filter", lead, "cutoff", "resonance") is two settings of `lead`; xy_pad("spot", [0, 100], [0, 50]) is
1544
+ two plain ranges (a third number in a range is where it starts; without one, the middle). `pad.x` and `pad.y`
1545
+ are the numbers and on_change(x, y) hears them. spring=True lets it go back when it is let go."""
1546
+ return XYPad(label, a, b, c, _options(spring=spring, x=x, y=y, size=size, share=share, span=span, colour=colour,
1547
+ color=color, on_change=on_change))
1548
+
1549
+
1550
+ def colour_picker(label="", a=None, b=None, *, value=None, colours=None, x=None, y=None, share=None, span=None,
1551
+ colour=None, color=None, on_change=None):
1552
+ """A patch of colour that opens a picker: `ink = colour_picker("ink", "tomato")`, then circle(x, y, 40, ink.value).
1553
+
1554
+ In the picker, drag in the square for how strong and how bright, along the strip for the hue, or tap a named
1555
+ colour. colours= is your own list of named colours, or False for none; colour_picker("ink", pen, "colour") is a
1556
+ setting of `pen`."""
1557
+ if a is None and value is None:
1558
+ a = "tomato"
1559
+ return ColourPicker(label, a, b, _options(value=value, colours=colours, x=x, y=y, share=share, span=span,
1560
+ colour=colour, color=color, on_change=on_change))
1561
+
1562
+
1563
+ color_picker = colour_picker
1564
+
1565
+
1566
+ def progress(label="", source=None, setting=None, *, value=None, min=None, max=None, percent=None, size=None, width=None,
1567
+ x=None, y=None, share=None, span=None, colour=None, color=None):
1568
+ """A bar that fills: `bar = progress("loading")` then bar.value = 0.4, or progress("loading", lambda: done / total).
1569
+
1570
+ It runs from 0 to 1 unless min= and max= say otherwise; percent=False leaves the number out."""
1571
+ return Progress(source, setting, _options(label=label, value=value, min=min, max=max, percent=percent, size=size,
1572
+ width=width, x=x, y=y, share=share, span=span, colour=colour, color=color))
1573
+
1574
+
1575
+ def light(label="", source=None, setting=None, *, value=None, size=None, x=None, y=None, share=None, span=None,
1576
+ colour=None, color=None):
1577
+ """A light: light("ready", True), then ready.on = False; light("signal", lead) is as bright as `lead` is loud;
1578
+ `lamp.value = "red"` shines that colour, and a number from 0 to 1 is how bright."""
1579
+ return Light(source, setting, _options(label=label, value=value, size=size, x=x, y=y, share=share, span=span,
1580
+ colour=colour, color=color))
1581
+
1582
+
1583
+ def busy(label="", source=None, *, value=None, size=None, x=None, y=None, share=None, span=None, colour=None, color=None):
1584
+ """A ring that goes round while something is happening: `wait = busy("loading")`, then wait.on = False."""
1585
+ return Busy(source if source is not None else True, None, _options(label=label, value=value, size=size, x=x, y=y,
1586
+ share=share, span=span, colour=colour, color=color))
1587
+
1588
+
1589
+ def range_slider(label=None, a=None, b=None, c=None, *, min=None, max=None, value=None, step=None, unit=None, digits=None,
1590
+ vertical=None, x=None, y=None, size=None, share=None, span=None, colour=None, color=None, on_change=None):
1591
+ """A slider with a low and a high handle: `price = range_slider("price", 0, 100, [20, 80])`, then price.low and
1592
+ price.high. range_slider("loop", sample, "start", "end") is two settings of `sample`. on_change(low, high)."""
1593
+ return RangeSlider(label, a, b, c, _options(min=min, max=max, value=value, step=step, unit=unit, digits=digits,
1594
+ vertical=vertical, x=x, y=y, size=size, share=share, span=span,
1595
+ colour=colour, color=color, on_change=on_change))
1596
+
1597
+
1598
+ def fader(label=None, a=None, b=None, c=None, *, min=None, max=None, value=None, step=None, unit=None, log=None,
1599
+ digits=None, default=None, x=None, y=None, size=None, share=None, span=None, colour=None, color=None,
1600
+ on_change=None):
1601
+ """An upright slider with the look of a mixing desk's: fader("volume", lead, "volume") or fader("level", 0, 1, 0.7)."""
1602
+ return Fader(label, a, b, c, _options(min=min, max=max, value=value, step=step, unit=unit, log=log, digits=digits,
1603
+ default=default, x=x, y=y, size=size, share=share, span=span, colour=colour,
1604
+ color=color, on_change=on_change))
1605
+
1606
+
1607
+ # ---------------------------------------------------------------- for games
1608
+
1609
+ class Joystick(Control):
1610
+ """A stick under one thumb. `x` and `y` are each from -1 to 1 (y down the picture), and 0 when it is let go."""
1611
+ kind = "joystick"
1612
+
1613
+ def __init__(self, spec):
1614
+ self.x = self.y = 0.0
1615
+ Control.__init__(self, spec)
1616
+
1617
+ value = property(lambda self: [self.x, self.y])
1618
+ angle = property(lambda self: math.atan2(self.y, self.x), doc="The way it is pushed, in radians: 0 to the right, as math.atan2 gives it.")
1619
+ distance = property(lambda self: min(1.0, math.hypot(self.x, self.y)), doc="How far it is pushed, from 0 to 1.")
1620
+
1621
+ def _told(self, pair):
1622
+ self.x, self.y = pair
1623
+ if self.on_change:
1624
+ self.on_change(self.x, self.y)
1625
+
1626
+ def __iter__(self):
1627
+ return iter((self.x, self.y))
1628
+
1629
+
1630
+ class Dpad(Control):
1631
+ """A pad of arrows. `left`, `right`, `up` and `down` are True while pressed; `x` and `y` are -1, 0 or 1."""
1632
+ kind = "dpad"
1633
+
1634
+ def __init__(self, spec):
1635
+ self.left = self.right = self.up = self.down = False
1636
+ Control.__init__(self, spec)
1637
+
1638
+ x = property(lambda self: self.right - self.left)
1639
+ y = property(lambda self: self.down - self.up, doc="1 while down is pressed and -1 while up is: y goes down the picture.")
1640
+ value = property(lambda self: [self.x, self.y])
1641
+
1642
+ def _told(self, arrows):
1643
+ # Which arrows are down, as one number: left 1, right 2, up 4, down 8.
1644
+ arrows = int(arrows)
1645
+ self.left, self.right, self.up, self.down = bool(arrows & 1), bool(arrows & 2), bool(arrows & 4), bool(arrows & 8)
1646
+ if self.on_change:
1647
+ self.on_change(self.x, self.y)
1648
+
1649
+ def __iter__(self):
1650
+ return iter((self.x, self.y))
1651
+
1652
+
1653
+ class ActionButton(Button):
1654
+ """A round button, as a game pad's A and B are."""
1655
+ kind = "actionButton"
1656
+
1657
+
1658
+ def joystick(label=None, *, floating=None, as_keys=None, size=None, width=None, height=None, x=None, y=None, share=None,
1659
+ span=None, colour=None, color=None, on_change=None):
1660
+ """A stick for a thumb, which springs back when it is let go: `stick = joystick()`, then in update()
1661
+ `ship.x += stick.x * speed * dt` and `ship.y += stick.y * speed * dt`.
1662
+
1663
+ `stick.x` and `stick.y` are from -1 to 1, in the scene's own directions (y goes DOWN the picture);
1664
+ `stick.angle` is the way it is pushed and `stick.distance` how far. floating=True: it appears where the thumb
1665
+ lands, anywhere in its box (width=, height=). as_keys=True: pushed, it holds the arrow keys down for the scene."""
1666
+ return Joystick(_options(label=label, floating=floating, as_keys=as_keys, size=size, width=width, height=height, x=x, y=y,
1667
+ share=share, span=span, colour=colour, color=color, on_change=on_change))
1668
+
1669
+
1670
+ def dpad(label=None, *, ways=None, as_keys=None, size=None, x=None, y=None, share=None, span=None, colour=None, color=None,
1671
+ on_change=None):
1672
+ """A pad of arrows under one thumb: `pad = dpad()`, then `if pad.left:` or `x += pad.x * speed * dt`.
1673
+
1674
+ as_keys=True makes it the arrow keys, so a game written with `"ArrowLeft" in keys` is played on a phone
1675
+ unchanged (as_keys="wasd" for those keys). Between two arrows both are pressed, unless ways=4."""
1676
+ return Dpad(_options(label=label, ways=ways, as_keys=as_keys, size=size, x=x, y=y, share=share, span=span, colour=colour,
1677
+ color=color, on_change=on_change))
1678
+
1679
+
1680
+ def action_button(label="", on_press=None, *, key=None, on_release=None, size=None, x=None, y=None, share=None, span=None,
1681
+ colour=None, color=None):
1682
+ """A round button: `fire = action_button("A")`, then `if fire.pressed:` in update(), or `fire.held`.
1683
+
1684
+ key="space" (or " ", "a", "Enter") holds that key down for the scene while it is pressed. Each finger holds
1685
+ its own control, so a thumb can steer while another fires."""
1686
+ return ActionButton(_options(label=label, on_press=on_press, on_release=on_release, key=key, size=size, x=x, y=y,
1687
+ share=share, span=span, colour=colour, color=color))
1688
+
1689
+
1690
+ # ---------------------------------------------------------------- for music
1691
+
1692
+ _SHAPE = ("attack", "decay", "sustain", "release")
1693
+
1694
+
1695
+ class Envelope(_Bound):
1696
+ """The shape of a note as a graph with corners to drag. `attack`, `decay`, `sustain` and `release` are the numbers."""
1697
+ kind = "envelope"
1698
+
1699
+ def __init__(self, label, target, names, spec):
1700
+ if label is not None and not isinstance(label, str):
1701
+ label, target = None, label
1702
+ spec["label"] = label
1703
+ self._params = tuple(names) if len(names) == 4 else None
1704
+ self._tie(spec, target, _SHAPE[0] if target is not None else None)
1705
+ self._v = [0.05, 0.3, 0.7, 0.5]
1706
+ if target is not None:
1707
+ spec["label"] = label # (the first setting's name is no heading for all four)
1708
+ if "target" in spec:
1709
+ del spec["param"]
1710
+ if self._params:
1711
+ spec["params"] = list(self._params)
1712
+ else:
1713
+ # A Python object: its four numbers are read here, with the ranges it gives, and the page is told each.
1714
+ self._params = self._params or _SHAPE
1715
+ self._v = []
1716
+ for part, name in zip(_SHAPE, self._params):
1717
+ d = _describe(target, name)
1718
+ now = _read(target, name)
1719
+ way = {"min": d.get("min", 0), "max": d.get("max", 1 if part == "sustain" else 2), "value": now if _is_number(now) else 0}
1720
+ if d.get("unit"):
1721
+ way["unit"] = d["unit"]
1722
+ spec[part] = way
1723
+ self._v.append(way["value"])
1724
+ else:
1725
+ for i, part in enumerate(_SHAPE):
1726
+ if _is_number(spec.get(part)):
1727
+ self._v[i] = spec[part]
1728
+ Control.__init__(self, spec)
1729
+
1730
+ @property
1731
+ def value(self):
1732
+ return list(self._v)
1733
+
1734
+ @value.setter
1735
+ def value(self, four):
1736
+ four = [_value_of(v) for v in four]
1737
+ if four == self._v:
1738
+ return
1739
+ self._v = four
1740
+ self._set("value", four)
1741
+ self._all(four)
1742
+
1743
+ def _all(self, four):
1744
+ names = self._params or _SHAPE
1745
+ if self._target is not None:
1746
+ for name, value in zip(names, four):
1747
+ _write(self._target, name, value)
1748
+ elif self._owner is not None and callable(getattr(self._owner, "_told", None)):
1749
+ for name, value in zip(names, four):
1750
+ self._owner._told(name, value)
1751
+
1752
+ def _made(self, low, high, default, choices, value):
1753
+ if isinstance(value, (list, tuple)):
1754
+ self._v = list(value)
1755
+
1756
+ def _told(self, four):
1757
+ self._v = list(four)
1758
+ self._all(self._v)
1759
+ if self.on_change:
1760
+ self.on_change(*self._v)
1761
+
1762
+ def _frame(self):
1763
+ if self._target is None:
1764
+ return
1765
+ there = [_read(self._target, name) for name in self._params]
1766
+ if all(_is_number(v) for v in there) and there != self._v:
1767
+ self._v = there
1768
+ self._set("value", there)
1769
+
1770
+ def __iter__(self):
1771
+ return iter(self._v)
1772
+
1773
+
1774
+ for _i, _part in enumerate(_SHAPE):
1775
+ setattr(Envelope, _part, property(lambda self, _i=_i: self._v[_i],
1776
+ lambda self, v, _i=_i: setattr(self, "value", self._v[:_i] + [v] + self._v[_i + 1:])))
1777
+
1778
+
1779
+ class StepGrid(Control):
1780
+ """A grid of steps: a row for each sound, a column for each step, every cell 0 (off) or how hard (1 unless told)."""
1781
+ kind = "stepGrid"
1782
+
1783
+ def __init__(self, spec):
1784
+ rows = spec.get("rows") or 1
1785
+ self.rows = [str(r) for r in rows] if isinstance(rows, (list, tuple)) else [str(i + 1) for i in range(rows)]
1786
+ self.steps = int(spec.get("steps") or 16)
1787
+ spec["rows"], spec["steps"] = self.rows, self.steps
1788
+ given = spec.get("value") or []
1789
+ self._cells = [[(given[r][s] if r < len(given) and s < len(given[r]) else 0) for s in range(self.steps)] for r in range(len(self.rows))]
1790
+ self._step = -1
1791
+ Control.__init__(self, spec)
1792
+
1793
+ def _row(self, row):
1794
+ return row if isinstance(row, int) else self.rows.index(str(row))
1795
+
1796
+ @property
1797
+ def value(self):
1798
+ return [list(row) for row in self._cells]
1799
+
1800
+ def get(self, row, step):
1801
+ """One cell: 0 for off, else how hard. A row is named or numbered."""
1802
+ return self._cells[self._row(row)][step]
1803
+
1804
+ def row(self, row):
1805
+ """A whole row, by its name or its number."""
1806
+ return list(self._cells[self._row(row)])
1807
+
1808
+ def put(self, row, step, on=True):
1809
+ """Turn one cell on or off (or say how hard, from 0 to 1)."""
1810
+ r, level = self._row(row), (1 if on is True else max(0, min(1, _value_of(on) or 0)))
1811
+ if 0 <= step < self.steps and self._cells[r][step] != level:
1812
+ self._cells[r][step] = level
1813
+ self._call("put", r, step, level)
1814
+
1815
+ def clear(self):
1816
+ for row in self._cells:
1817
+ row[:] = [0] * self.steps
1818
+ self._call("clear")
1819
+
1820
+ @property
1821
+ def step(self):
1822
+ """The column that is lit as the one playing (none: -1)."""
1823
+ return self._step
1824
+
1825
+ @step.setter
1826
+ def step(self, n):
1827
+ n = -1 if n is None or n < 0 else int(n) % self.steps
1828
+ if n != self._step:
1829
+ self._step = n
1830
+ self._set("step", n)
1831
+
1832
+ def _told(self, cell):
1833
+ r, s, level = cell
1834
+ self._cells[int(r)][int(s)] = level
1835
+ if self.on_change:
1836
+ self.on_change(self.rows[int(r)], int(s), level)
1837
+
1838
+
1839
+ class Pads(Control):
1840
+ """Pads to hit, several at once. `held` is the names of those down now."""
1841
+ kind = "pads"
1842
+
1843
+ def __init__(self, target, spec):
1844
+ self.on_note_on = spec.pop("on_note_on", None)
1845
+ self.on_note_off = spec.pop("on_note_off", None)
1846
+ names = spec.get("pads") or 4
1847
+ self.names = [str(n) for n in names] if isinstance(names, (list, tuple)) else [str(i + 1) for i in range(names)]
1848
+ spec["pads"] = self.names
1849
+ drums = {"kick": 36, "rim": 37, "snare": 38, "clap": 39, "tom": 45, "hat": 42, "closed hat": 42, "open hat": 46, "crash": 49, "ride": 51, "cowbell": 56}
1850
+ given = spec.get("notes") or []
1851
+ self.notes = [given[i] if i < len(given) else drums.get(name.lower(), 36 + i) for i, name in enumerate(self.names)]
1852
+ spec["notes"] = self.notes
1853
+ self._down = set()
1854
+ page = _in_page(target)
1855
+ self._target = None if page is not None else target
1856
+ if page is not None:
1857
+ spec["target"] = page
1858
+ Control.__init__(self, spec)
1859
+
1860
+ @property
1861
+ def held(self):
1862
+ return [name for name, note in zip(self.names, self.notes) if note in self._down]
1863
+
1864
+ value = held
1865
+
1866
+ def hit(self, pad, velocity=0.8):
1867
+ """Hit a pad as a finger would, by its name or its place; lift() lets it go again."""
1868
+ self._call("hit", pad, velocity)
1869
+
1870
+ def lift(self, pad):
1871
+ self._call("lift", pad)
1872
+
1873
+ def flash(self, pad):
1874
+ """Light a pad for a moment without playing it."""
1875
+ self._call("flash", pad)
1876
+
1877
+ def _played(self, note, velocity):
1878
+ name = self.names[self.notes.index(note)] if note in self.notes else None
1879
+ if velocity:
1880
+ self._down.add(note)
1881
+ play = _method(self._target, "note_on", "noteOn")
1882
+ if play:
1883
+ play(note, velocity)
1884
+ if self.on_note_on:
1885
+ self.on_note_on(note, velocity, name)
1886
+ else:
1887
+ self._down.discard(note)
1888
+ stop = _method(self._target, "note_off", "noteOff")
1889
+ if stop:
1890
+ stop(note)
1891
+ if self.on_note_off:
1892
+ self.on_note_off(note, name)
1893
+
1894
+
1895
+ class Wheel(_Ranged):
1896
+ kind = "wheel"
1897
+
1898
+
1899
+ def envelope(label=None, target=None, *names, attack=None, decay=None, sustain=None, release=None, width=None, height=None,
1900
+ x=None, y=None, share=None, span=None, colour=None, color=None, on_change=None):
1901
+ """How a note begins, settles, holds and dies away, as a graph whose corners are dragged.
1902
+
1903
+ envelope("shape", lead) finds lead's attack, decay, sustain and release by those names;
1904
+ envelope("shape", lead, "a", "d", "s", "r") is told the four names; with no target, `shape.attack`,
1905
+ `shape.decay`, `shape.sustain` and `shape.release` are numbers to use, starting where attack= and the rest say.
1906
+ on_change(attack, decay, sustain, release) hears a hand."""
1907
+ return Envelope(label, target, names, _options(attack=attack, decay=decay, sustain=sustain, release=release, width=width,
1908
+ height=height, x=x, y=y, share=share, span=span, colour=colour,
1909
+ color=color, on_change=on_change))
1910
+
1911
+
1912
+ def step_grid(label=None, steps=None, rows=None, *, value=None, velocity=None, x=None, y=None, share=None, span=None,
1913
+ colour=None, color=None, on_change=None):
1914
+ """A grid of steps, each on or off: `grid = step_grid("drums", 16, ["kick", "snare", "hat"])`.
1915
+
1916
+ `grid.value` is the rows, each a list of 0 and 1; `grid.get("kick", 0)` reads a cell and `grid.put("kick", 0)`
1917
+ writes one; `grid.step = 3` lights that column as the one playing. A tap turns a cell over, and a finger
1918
+ dragged along turns on (or off) every cell it crosses. velocity=True: a drag up or down on a cell says how
1919
+ hard. on_change(row, step, on)."""
1920
+ if label is not None and not isinstance(label, str):
1921
+ label, steps, rows = None, label, steps
1922
+ return StepGrid(_options(label=label, steps=steps, rows=rows, value=value, velocity=velocity, x=x, y=y, share=share,
1923
+ span=span, colour=colour, color=color, on_change=on_change))
1924
+
1925
+
1926
+ def pads(label=None, names=None, target=None, *, notes=None, columns=None, size=None, x=None, y=None, share=None, span=None,
1927
+ colour=None, color=None, on_note_on=None, on_note_off=None):
1928
+ """Pads to hit, several at once: pads("kit", ["kick", "snare", "hat", "clap"], drums) plays `drums`.
1929
+
1930
+ Each plays a note for as long as it is held, harder the nearer its middle it is hit. Pads named for drums
1931
+ play those drums' notes (a kick is 36); others count up from 36, or are told with notes=.
1932
+ on_note_on(note, velocity, name) hears them, and `kit.flash("kick")` lights one from your code."""
1933
+ if label is not None and not isinstance(label, str):
1934
+ label, names, target = None, label, names
1935
+ return Pads(target, _options(label=label, pads=names, notes=notes, columns=columns, size=size, x=x, y=y, share=share,
1936
+ span=span, colour=colour, color=color, on_note_on=on_note_on, on_note_off=on_note_off))
1937
+
1938
+
1939
+ def wheel(label=None, a=None, b=None, c=None, *, spring=None, min=None, max=None, value=None, step=None, unit=None,
1940
+ digits=None, default=None, x=None, y=None, size=None, share=None, span=None, colour=None, color=None,
1941
+ on_change=None):
1942
+ """A wheel that is rolled, as at the left of a synth's keys: wheel("mod", lead, "mod"). spring=True lets it roll
1943
+ back to where it started when it is let go."""
1944
+ return Wheel(label, a, b, c, _options(spring=spring, min=min, max=max, value=value, step=step, unit=unit, digits=digits,
1945
+ default=default, x=x, y=y, size=size, share=share, span=span, colour=colour,
1946
+ color=color, on_change=on_change))
1947
+
1948
+
1949
+ mod_wheel = wheel
1950
+
1951
+
1952
+ def pitch_wheel(label=None, a=None, b=None, c=None, **options):
1953
+ """A wheel that rolls back to the middle when it is let go: pitch_wheel("bend", lead, "bend"), or
1954
+ `bend = pitch_wheel("bend")`, which runs from -1 to 1."""
1955
+ if a is None and options.get("min") is None:
1956
+ a, b, c = -1, 1, 0
1957
+ return wheel(label, a, b, c, spring=True, **options)
1958
+
1959
+
1960
+ # ---------------------------------------------------------------- patterns
1961
+ # A pattern as text: the text the sound engine reads, by the very marks the page's half uses (change a mark
1962
+ # in both tables together).
1963
+ #
1964
+ # steps (a gate, a drum's row, a lane): one mark a step. . off x on 1 to 8: on, at that many ninths
1965
+ # (0 is off and 9 is on). X on, with an accent: a drum's row and a
1966
+ # lane have accents, a gate has none. Spaces and | are for the eye.
1967
+ # an arpeggio: steps with a space between. 1 2 3: that note of the chord, counted from 1 at its lowest
1968
+ # (past its top note the count goes on an octave up) - or . a rest _ the note before, held on
1969
+ # after a note: ' an octave up (, down) ^ an accent :1 to :9 its own length, in tenths of a step
1970
+ # before a note: ~ slide into it
1971
+ #
1972
+ # In place of the steps a pattern may be the name of one that is built in ("trance", "gallop").
1973
+
1974
+ # A gate's text and a row of steps' text are two kinds of text, though they look alike: a gate's is the sound
1975
+ # engine's and has no accents; a drum's row (and a lane) is this library's own, and has. Neither is the other.
1976
+ GATE_MARKS = {"off": ".", "on": "x", "accent": None, "skipped": " ", "parts": 9}
1977
+ STEP_MARKS = {"off": ".", "on": "x", "accent": "X", "skipped": " |", "parts": 9}
1978
+ ARP_MARKS = {"rest": "-", "rests": "-.", "tie": "_", "up": "'", "down": ",", "accent": "^", "length": ":", "slide": "~"}
1979
+ GATE_NAMED = {"eighths": "x.x.x.x.x.x.x.x.", "offbeat": "..x...x...x...x.", "gallop": "x.xxx.xxx.xxx.xx", "trance": "x.x.xx.xx.x.xx.x",
1980
+ "stutter": "xxx.xx.xx.xxx.x.", "pump": "2468246824682468", "triplets": "x..x..x..x..x..x..x..x..", "dotted": "x..x..x.x..x..x."}
1981
+ ARP_NAMED = {"bounce": "1 2 3 2", "octaves": "1 1' 1 1'", "root-fifth": "1 3 1' 3", "thumb": "1 2 1 3 1 4 1 3", "converge": "1 4 2 3",
1982
+ "diverge": "2 3 1 4", "offbeat": "- 1 - 2 - 3 - 2", "gallop": "1^ - 1 1 2^ - 2 2", "trance": "1^ - 3 2 - 1^ 3 -",
1983
+ "acid": "1^ 1 ~3 - 1' ~1 2^ -"}
1984
+
1985
+
1986
+ def _named(given, names):
1987
+ """A pattern given as text may be the name of one: the text that name stands for, or what was given."""
1988
+ return names.get(given.strip().lower(), given) if isinstance(given, str) else given
1989
+
1990
+
1991
+ def _steps_from(given, marks=STEP_MARKS):
1992
+ """A row of steps, however it is written (text, numbers, True and False, or steps), as [level, accent] pairs."""
1993
+ if isinstance(given, str):
1994
+ out = []
1995
+ for mark in given:
1996
+ if mark in marks["skipped"]:
1997
+ continue
1998
+ if mark in "0123456789":
1999
+ out.append([min(1, int(mark) / marks["parts"]), False])
2000
+ else:
2001
+ out.append([0 if mark == marks["off"] else 1, bool(marks["accent"]) and mark == marks["accent"]])
2002
+ return out
2003
+ out = []
2004
+ for step in given or ():
2005
+ if isinstance(step, dict):
2006
+ out.append([step.get("level", 0 if step.get("on") is False else 1), bool(step.get("accent"))])
2007
+ else:
2008
+ out.append([1 if step is True else max(0, min(1, _value_of(step) or 0)), False])
2009
+ return out
2010
+
2011
+
2012
+ def _steps_text(steps, marks=STEP_MARKS):
2013
+ out, parts = [], marks["parts"]
2014
+ for level, accent in steps:
2015
+ part = int((level or 0) * parts + 0.5)
2016
+ out.append(marks["off"] if part <= 0 else marks["accent"] if accent and marks["accent"] else marks["on"] if part >= parts else str(part))
2017
+ return "".join(out)
2018
+
2019
+
2020
+ def _arp_step(**given):
2021
+ """One step of an arpeggio: a rest, until it is given a note. A length of 0 is the arpeggio's own."""
2022
+ step = {"note": 0, "octave": 0, "tie": False, "accent": False, "slide": False, "length": 0}
2023
+ step.update(given)
2024
+ return step
2025
+
2026
+
2027
+ def _arp_word(word):
2028
+ m = ARP_MARKS
2029
+ if word == m["tie"]:
2030
+ return _arp_step(tie=True)
2031
+ slide = word.startswith(m["slide"])
2032
+ rest = word[len(m["slide"]):] if slide else word
2033
+ digits = ""
2034
+ while rest[:1].isdigit() and len(digits) < 2:
2035
+ digits, rest = digits + rest[0], rest[1:]
2036
+ if not digits:
2037
+ return _arp_step() # a rest, and whatever is not a step
2038
+ octave = 0
2039
+ while rest[:1] in (m["up"], m["down"]):
2040
+ octave += 1 if rest[0] == m["up"] else -1
2041
+ rest = rest[1:]
2042
+ accent = rest.startswith(m["accent"])
2043
+ rest = rest[1:] if accent else rest
2044
+ length = int(rest[1]) if len(rest) == 2 and rest[0] == m["length"] and rest[1].isdigit() else 0
2045
+ return _arp_step(note=int(digits), octave=octave, accent=accent, slide=slide, length=length)
2046
+
2047
+
2048
+ def _arp_from(given):
2049
+ out = []
2050
+ for word in (_named(given, ARP_NAMED).split() if isinstance(given, str) else list(given or ())):
2051
+ if isinstance(word, dict):
2052
+ out.append(_arp_step(**{k: word[k] for k in _arp_step() if k in word}))
2053
+ elif isinstance(word, str):
2054
+ out.append(_arp_word(word))
2055
+ else:
2056
+ out.append(_arp_step(note=int(word or 0)))
2057
+ return out
2058
+
2059
+
2060
+ def _arp_text(steps):
2061
+ m, words = ARP_MARKS, []
2062
+ for s in steps:
2063
+ if s["tie"]:
2064
+ words.append(m["tie"])
2065
+ elif not s["note"]:
2066
+ words.append(m["rest"])
2067
+ else:
2068
+ words.append((m["slide"] if s["slide"] else "") + str(s["note"]) + (m["up"] * s["octave"] if s["octave"] > 0 else m["down"] * -s["octave"])
2069
+ + (m["accent"] if s["accent"] else "") + (m["length"] + str(s["length"]) if s.get("length") else ""))
2070
+ return " ".join(words)
2071
+
2072
+
2073
+ class Steps(_Bound):
2074
+ """A lane of steps. `value` is their levels (0 for off), `steps` each as a dict, `text` the same as text."""
2075
+ kind = "steps"
2076
+ _names = ("pattern", "steps")
2077
+ _presets = {} # the patterns it can be given by name
2078
+ _marks = STEP_MARKS # how it is written as text
2079
+
2080
+ def __init__(self, label, a, b, spec):
2081
+ if label is not None and not isinstance(label, str):
2082
+ label, a, b = None, label, a
2083
+ spec["label"] = label
2084
+ target = None
2085
+ if _is_number(a):
2086
+ spec.setdefault("steps", int(a))
2087
+ elif isinstance(a, (str, list, tuple)):
2088
+ spec.setdefault("value", a)
2089
+ elif a is not None:
2090
+ target = a
2091
+ self._as_text = True
2092
+ param = b if isinstance(b, str) else None
2093
+ if target is not None and param is None and _in_page(target) is None:
2094
+ param = next((name for name in self._names if _read(target, name) is not None), self._names[0])
2095
+ self._tie(spec, target, param)
2096
+ if "target" in spec and param is None:
2097
+ del spec["param"] # the page finds it by name
2098
+ self._param = self._names[0] # and the Python object that stands for the target hears it by its Python name
2099
+ if self._target is not None:
2100
+ there = _read(self._target, self._param)
2101
+ self._as_text = isinstance(there, str)
2102
+ spec["label"] = label or spec.get("label")
2103
+ if spec.get("value") is None and there not in (None, ""):
2104
+ spec["value"] = there
2105
+ self._cells = _steps_from(_named(spec.get("value"), self._presets), self._marks)
2106
+ if not self._cells:
2107
+ self._cells = [[0, False] for _ in range(spec.get("steps") or 16)]
2108
+ elif spec.get("steps"):
2109
+ self._cells = (self._cells + [[0, False]] * spec["steps"])[:spec["steps"]]
2110
+ if "target" not in spec:
2111
+ spec["value"] = _steps_text(self._cells, self._marks)
2112
+ spec["steps"] = len(self._cells)
2113
+ self._step = -1
2114
+ Control.__init__(self, spec)
2115
+
2116
+ value = property(lambda self: [level for level, accent in self._cells])
2117
+ steps = property(lambda self: [{"on": level > 0, "level": level, "accent": accent} for level, accent in self._cells])
2118
+ text = property(lambda self: _steps_text(self._cells, self._marks))
2119
+ length = property(lambda self: len(self._cells))
2120
+
2121
+ def _take(self, given):
2122
+ self._cells = _steps_from(_named(given, self._presets), self._marks)
2123
+ self._set("value", self.text)
2124
+ self._out_to_target()
2125
+
2126
+ def preset(self, name):
2127
+ """Write a built-in pattern by its name: gate.preset("pump")."""
2128
+ if name in self._presets:
2129
+ self._take(name)
2130
+
2131
+ value = value.setter(_take)
2132
+ steps = steps.setter(_take)
2133
+ text = text.setter(_take)
2134
+
2135
+ @length.setter
2136
+ def length(self, n):
2137
+ self._take((self._cells + [[0, False]] * n)[:max(1, int(n))])
2138
+
2139
+ def put(self, i, on=True, accent=False):
2140
+ """Turn one step on or off (or say its level, from 0 to 1)."""
2141
+ cells = [list(c) for c in self._cells]
2142
+ cells[i] = [1 if on is True else max(0, min(1, _value_of(on) or 0)), bool(accent)]
2143
+ if cells != self._cells:
2144
+ self._take([{"level": level, "accent": accent} for level, accent in cells])
2145
+
2146
+ def clear(self):
2147
+ self._take([0] * len(self._cells))
2148
+
2149
+ @property
2150
+ def step(self):
2151
+ """The step that is lit as the one sounding (none: -1)."""
2152
+ return self._step
2153
+
2154
+ @step.setter
2155
+ def step(self, n):
2156
+ n = -1 if n is None or n < 0 else int(n) % max(1, len(self._cells))
2157
+ if n != self._step:
2158
+ self._step = n
2159
+ self._set("step", n)
2160
+
2161
+ def _out_to_target(self):
2162
+ self._send(self.text if self._as_text else self.value)
2163
+
2164
+ def _made(self, low, high, default, choices, value):
2165
+ if isinstance(value, str):
2166
+ self._cells = _steps_from(value, self._marks)
2167
+
2168
+ def _told(self, text):
2169
+ self._cells = _steps_from(text, self._marks)
2170
+ self._out_to_target()
2171
+ if self.on_change:
2172
+ self.on_change(self.value)
2173
+
2174
+ def _frame(self):
2175
+ if self._target is None:
2176
+ return
2177
+ there = _read(self._target, self._param)
2178
+ if there not in (None, "") and _steps_text(_steps_from(_named(there, self._presets), self._marks), self._marks) != self.text:
2179
+ self._cells = _steps_from(_named(there, self._presets), self._marks)
2180
+ self._set("value", self.text)
2181
+
2182
+ def __iter__(self):
2183
+ return iter(self.value)
2184
+
2185
+ def __len__(self):
2186
+ return len(self._cells)
2187
+
2188
+
2189
+ class TranceGate(Steps):
2190
+ """A gate's pattern: steps that are bars, each as tall as the gate is open."""
2191
+ kind = "tranceGate"
2192
+ _names = ("trance_gate", "tranceGate", "pattern")
2193
+ _presets = GATE_NAMED
2194
+ _marks = GATE_MARKS
2195
+ _plated = True
2196
+
2197
+
2198
+ class ArpPattern(_Bound):
2199
+ """An arpeggio's pattern. `value` is its steps, each a dict (note 0 is a rest), and `text` the same as text."""
2200
+ kind = "arpPattern"
2201
+ _plated = True
2202
+
2203
+ def __init__(self, label, a, b, spec):
2204
+ if label is not None and not isinstance(label, str):
2205
+ label, a, b = None, label, a
2206
+ spec["label"] = label
2207
+ target = None
2208
+ if isinstance(a, (str, list, tuple)):
2209
+ spec.setdefault("value", a)
2210
+ elif a is not None:
2211
+ target = a
2212
+ self._as_text = True
2213
+ param = b if isinstance(b, str) else None
2214
+ if target is not None and param is None and _in_page(target) is None:
2215
+ param = next((name for name in ("arpPattern", "arp_pattern", "pattern") if _read(target, name) is not None), "arpPattern")
2216
+ self._tie(spec, target, param)
2217
+ if "target" in spec and param is None:
2218
+ del spec["param"]
2219
+ self._param = "arp_pattern"
2220
+ if self._target is not None:
2221
+ there = _read(self._target, self._param)
2222
+ self._as_text = isinstance(there, str)
2223
+ spec["label"] = label or spec.get("label")
2224
+ if spec.get("value") is None and there not in (None, ""):
2225
+ spec["value"] = there
2226
+ self._steps = _arp_from(spec.get("value"))
2227
+ if "target" not in spec and self._steps:
2228
+ spec["value"] = _arp_text(self._steps)
2229
+ self._step = -1
2230
+ Control.__init__(self, spec)
2231
+
2232
+ value = property(lambda self: [dict(step) for step in self._steps])
2233
+ text = property(lambda self: _arp_text(self._steps))
2234
+ length = property(lambda self: len(self._steps))
2235
+
2236
+ def _take(self, given):
2237
+ self._steps = _arp_from(given)
2238
+ self._set("value", self.text)
2239
+ self._send(self.text if self._as_text else self.value)
2240
+
2241
+ value = value.setter(_take)
2242
+ text = text.setter(_take)
2243
+
2244
+ def preset(self, name):
2245
+ """Write a built-in pattern by its name: arp.preset("trance")."""
2246
+ if name in ARP_NAMED:
2247
+ self._take(name)
2248
+
2249
+ step = Steps.step
2250
+
2251
+ def _made(self, low, high, default, choices, value):
2252
+ if isinstance(value, str):
2253
+ self._steps = _arp_from(value)
2254
+
2255
+ def _told(self, text):
2256
+ self._steps = _arp_from(text)
2257
+ self._send(self.text if self._as_text else self.value)
2258
+ if self.on_change:
2259
+ self.on_change(self.value)
2260
+
2261
+ def _frame(self):
2262
+ if self._target is None:
2263
+ return
2264
+ there = _read(self._target, self._param)
2265
+ if there not in (None, "") and _arp_text(_arp_from(there)) != self.text:
2266
+ self._steps = _arp_from(there)
2267
+ self._set("value", self.text)
2268
+
2269
+ def __len__(self):
2270
+ return len(self._steps)
2271
+
2272
+
2273
+ class DrumSeq(Control):
2274
+ """A drum sequencer that plays by itself. `value` is its rows of levels, `text` a line of marks for each drum."""
2275
+ kind = "drumSeq"
2276
+
2277
+ def __init__(self, rows, target, spec):
2278
+ self.on_step = spec.pop("on_step", None)
2279
+ self.rows = [str(r) for r in (rows or ["kick", "snare", "hat", "clap"])]
2280
+ spec["rows"] = self.rows
2281
+ self._count = int(spec.get("steps") or 16)
2282
+ # A target in the page is played there, on its own clock; a Python one is played here, a frame after its step.
2283
+ each = spec.pop("targets", None)
2284
+ each = [each.get(name) for name in self.rows] if isinstance(each, dict) else list(each or [])
2285
+ each = [(each[i] if i < len(each) and each[i] is not None else target) for i in range(len(self.rows))]
2286
+ self._targets = [None if _in_page(t) is not None else t for t in each]
2287
+ if any(_in_page(t) is not None for t in each):
2288
+ spec["targets"] = [_in_page(t) for t in each]
2289
+ if _in_page(spec.get("clock")) is not None:
2290
+ spec["clock"] = _in_page(spec["clock"])
2291
+ else:
2292
+ spec.pop("clock", None)
2293
+ drums = {"kick": 36, "rim": 37, "snare": 38, "clap": 39, "tom": 45, "hat": 42, "closed hat": 42, "open hat": 46, "crash": 49, "ride": 51, "cowbell": 56}
2294
+ given = spec.get("notes") or []
2295
+ self.notes = [given[i] if i < len(given) else drums.get(name.lower(), 36 + i) for i, name in enumerate(self.rows)]
2296
+ spec["notes"] = self.notes
2297
+ self._lanes = [[[0, False] for _ in range(self._count)] for _ in self.rows]
2298
+ if spec.get("value") is not None:
2299
+ self._read(spec["value"])
2300
+ spec["value"] = self.text
2301
+ self._tempo = spec.get("tempo") or 120
2302
+ self._swing = spec.get("swing") or 0
2303
+ self._playing = bool(spec.get("playing"))
2304
+ self._step = -1
2305
+ self.muted = [False] * len(self.rows)
2306
+ Control.__init__(self, spec)
2307
+
2308
+ def _read(self, given):
2309
+ lines = [line.split(":")[-1] for line in given.split("\n") if line.strip()] if isinstance(given, str) else list(given or [])
2310
+ for r in range(len(self.rows)):
2311
+ row = _steps_from(lines[r]) if r < len(lines) else []
2312
+ self._lanes[r] = (row + [[0, False]] * self._count)[:self._count]
2313
+
2314
+ def _row(self, row):
2315
+ return row if isinstance(row, int) else self.rows.index(str(row))
2316
+
2317
+ value = property(lambda self: [[level for level, accent in lane] for lane in self._lanes])
2318
+ text = property(lambda self: "\n".join(_steps_text(lane) for lane in self._lanes))
2319
+
2320
+ def _take(self, given):
2321
+ self._read(given)
2322
+ self._set("value", self.text)
2323
+
2324
+ value = value.setter(_take)
2325
+ text = text.setter(_take)
2326
+
2327
+ def get(self, row, step):
2328
+ """One step of one drum: 0 for off, else how hard."""
2329
+ return self._lanes[self._row(row)][step][0]
2330
+
2331
+ def put(self, row, step, on=True, accent=False):
2332
+ self._lanes[self._row(row)][step] = [1 if on is True else max(0, min(1, _value_of(on) or 0)), bool(accent)]
2333
+ self._set("value", self.text)
2334
+
2335
+ def clear(self):
2336
+ self._take([])
2337
+
2338
+ def mute(self, row, on=True):
2339
+ self.muted[self._row(row)] = bool(on)
2340
+ self._call("mute", self._row(row), bool(on))
2341
+
2342
+ def play(self):
2343
+ self.playing = True
2344
+
2345
+ def stop(self):
2346
+ self.playing = False
2347
+
2348
+ def _sets(name, clean):
2349
+ def get(self):
2350
+ return getattr(self, "_" + name)
2351
+
2352
+ def put(self, value):
2353
+ value = clean(_value_of(value))
2354
+ if value != getattr(self, "_" + name):
2355
+ setattr(self, "_" + name, value)
2356
+ self._set(name, value)
2357
+ return property(get, put)
2358
+
2359
+ tempo = _sets("tempo", lambda v: max(30, min(300, v)))
2360
+ swing = _sets("swing", lambda v: max(0, min(0.75, v)))
2361
+ playing = _sets("playing", bool)
2362
+ del _sets
2363
+
2364
+ @property
2365
+ def step(self):
2366
+ """The step that is sounding, as the page last said (none: -1)."""
2367
+ return self._step
2368
+
2369
+ def pattern(self, name):
2370
+ """Start from a pattern by its name: "four on the floor", "backbeat" or "breakbeat"."""
2371
+ self._set("pattern", name)
2372
+
2373
+ def _told(self, news):
2374
+ if isinstance(news, str):
2375
+ self._read(news)
2376
+ if self.on_change:
2377
+ self.on_change(self.value)
2378
+ return
2379
+ what = news[0]
2380
+ if what == "step":
2381
+ r, s, velocity = int(news[1]), int(news[2]), news[3]
2382
+ self._step = s
2383
+ play = _method(self._targets[r], "note_on", "noteOn")
2384
+ if play:
2385
+ play(self.notes[r], velocity)
2386
+ if self.on_step:
2387
+ self.on_step(self.rows[r], s, velocity)
2388
+ elif what == "tempo":
2389
+ self._tempo = news[1]
2390
+ elif what == "swing":
2391
+ self._swing = news[1]
2392
+ elif what == "play":
2393
+ self._playing = bool(news[1])
2394
+ if not self._playing:
2395
+ self._step = -1
2396
+ elif what == "mute":
2397
+ self.muted[int(news[1])] = bool(news[2])
2398
+
2399
+
2400
+ def steps(label=None, a=None, b=None, *, value=None, max=None, levels=None, accents=None, velocity=None, length=None,
2401
+ x=None, y=None, share=None, span=None, colour=None, color=None, on_change=None):
2402
+ """A lane of steps to tap on and off and to drag across: `lane = steps("accent", 16)`.
2403
+
2404
+ `lane.value` is the levels (0 for off), `lane.steps` each as a dict and `lane.text` the same as text
2405
+ ("x...x.5."); any of the three may be set. `lane.length` is how many are in use (the − and + change it, up to
2406
+ max=), and `lane.step = 3` lights the one that is sounding. levels=True makes each a bar whose height is its
2407
+ level. steps("lane", thing, "pattern") reads and writes a pattern a target keeps, as text or as a list."""
2408
+ return Steps(label, a, b, _options(value=value, max=max, levels=levels, accents=accents, velocity=velocity, length=length,
2409
+ x=x, y=y, share=share, span=span, colour=colour, color=color, on_change=on_change))
2410
+
2411
+
2412
+ def trance_gate(label=None, a=None, b=None, *, value=None, max=None, beside=None, x=None, y=None, share=None, span=None,
2413
+ colour=None, color=None, on_change=None):
2414
+ """A gate's pattern, each step a bar as tall as the gate is open: tap one on and off, drag up and down for its
2415
+ level, drag across to draw.
2416
+
2417
+ trance_gate("gate", pad) is pad's gate pattern, with its named patterns, depth, rate, smoothing and swing
2418
+ beside it (beside=False: the pattern alone); trance_gate("gate", 16) and trance_gate("gate", "x.x.x5x.") are
2419
+ ones to read: `gate.value` (levels from 0 to 1) and `gate.text` (a digit is a level in ninths).
2420
+ `gate.preset("pump")` writes a built-in pattern."""
2421
+ return TranceGate(label, a, b, _options(value=value, max=max, beside=beside, x=x, y=y, share=share, span=span,
2422
+ colour=colour, color=color, on_change=on_change))
2423
+
2424
+
2425
+ def arp_pattern(label=None, a=None, b=None, *, value=None, steps=None, max=None, notes=None,
2426
+ beside=None, x=None, y=None, share=None, span=None, colour=None, color=None, on_change=None):
2427
+ """An arpeggio's pattern: steps across, and the notes of the chord up the side (1 is its lowest; past its top
2428
+ note the count goes on an octave up).
2429
+
2430
+ Tap a cell and that step plays that note; tap it again for a rest; drag sideways to hold the note on. The lanes
2431
+ under the grid give a step an accent (^), a slide into it (~), an octave (') and a length of its own (:).
2432
+ arp_pattern("arp", lead) is lead's pattern, with its named patterns beside it;
2433
+ arp_pattern("arp", "1 - 3 2 _ 1^ ~3 5'") is one to read: `arp.value` (a dict for each step: note, octave, tie,
2434
+ accent, slide, length) and `arp.text`, the text the sound engine reads. `arp.preset("trance")` writes a
2435
+ built-in pattern; notes= is how many rows of notes it starts with (8)."""
2436
+ return ArpPattern(label, a, b, _options(value=value, steps=steps, max=max, notes=notes,
2437
+ beside=beside, x=x, y=y, share=share, span=span, colour=colour, color=color,
2438
+ on_change=on_change))
2439
+
2440
+
2441
+ def drum_seq(label=None, rows=None, target=None, *, steps=None, tempo=None, swing=None, pattern=None, value=None,
2442
+ playing=None, targets=None, notes=None, clock=None, x=None, y=None, share=None, span=None, colour=None,
2443
+ color=None, on_change=None, on_step=None):
2444
+ """A drum sequencer that plays by itself: `beat = drum_seq("beat", ["kick", "snare", "hat", "clap"], drums)`.
2445
+
2446
+ A row of steps for each drum, with play and stop, a tempo, swing, starter patterns and clear on the widget.
2447
+ Each row plays its drum's note on `target` (or on its own, with targets=); a target in the page that has a
2448
+ clock is played by the page, in time however busy Python is. on_step(row, step, velocity) hears every step
2449
+ (a frame later). `beat.value` is the rows, `beat.text` a line of marks for each ("x...x..."), and `beat.tempo`,
2450
+ `beat.swing`, `beat.playing`, `beat.play()` and `beat.stop()` work it from code."""
2451
+ if label is not None and not isinstance(label, str):
2452
+ label, rows, target = None, label, rows
2453
+ return DrumSeq(rows, target, _options(label=label, steps=steps, tempo=tempo, swing=swing, pattern=pattern, value=value,
2454
+ playing=playing, targets=targets, notes=notes, clock=clock, x=x, y=y, share=share,
2455
+ span=span, colour=colour, color=color, on_change=on_change, on_step=on_step))