circuitpython-keymanager 1.1.1__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.
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: circuitpython-keymanager
3
+ Version: 1.1.1
4
+ Summary: Tools to manage notes in musical applications. Includes note priority, arpeggiation, and sequencing.
5
+ Author-email: Cooper Dalrymple <me@dcdalrymple.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/relic-se/CircuitPython_KeyManager
8
+ Keywords: adafruit,blinka,circuitpython,micropython,relic_keymanager,synthesizer,music,keyboard,arpeggiator,sequencer,midi,notes
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Software Development :: Libraries
11
+ Classifier: Topic :: Software Development :: Embedded Systems
12
+ Classifier: Topic :: System :: Hardware
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Description-Content-Type: text/x-rst
16
+ License-File: LICENSE
17
+ Requires-Dist: Adafruit-Blinka
18
+ Requires-Dist: asyncio
19
+ Provides-Extra: optional
20
+ Requires-Dist: Adafruit-CircuitPython-Debouncer; extra == "optional"
21
+ Dynamic: license-file
22
+
23
+ Introduction
24
+ ============
25
+
26
+
27
+ .. image:: https://readthedocs.org/projects/circuitpython-keymanager/badge/?version=latest
28
+ :target: https://circuitpython-keymanager.readthedocs.io/
29
+ :alt: Documentation Status
30
+
31
+
32
+
33
+ .. image:: https://img.shields.io/discord/327254708534116352.svg
34
+ :target: https://adafru.it/discord
35
+ :alt: Discord
36
+
37
+
38
+ .. image:: https://github.com/relic-se/CircuitPython_KeyManager/workflows/Build%20CI/badge.svg
39
+ :target: https://github.com/relic-se/CircuitPython_KeyManager/actions
40
+ :alt: Build Status
41
+
42
+
43
+ .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
44
+ :target: https://github.com/astral-sh/ruff
45
+ :alt: Code Style: Ruff
46
+
47
+ Tools to manage notes in musical applications. Includes note priority, arpeggiation, and sequencing.
48
+
49
+
50
+ Dependencies
51
+ =============
52
+ This driver depends on:
53
+
54
+ * `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_
55
+
56
+ Please ensure all dependencies are available on the CircuitPython filesystem.
57
+ This is easily achieved by downloading
58
+ `the Adafruit library and driver bundle <https://circuitpython.org/libraries>`_
59
+ or individual libraries can be installed using
60
+ `circup <https://github.com/adafruit/circup>`_.
61
+
62
+ Installing from PyPI
63
+ =====================
64
+ .. note:: This library is not available on PyPI yet. Install documentation is included
65
+ as a standard element. Stay tuned for PyPI availability!
66
+
67
+ On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
68
+ PyPI <https://pypi.org/project/circuitpython-keymanager/>`_.
69
+ To install for current user:
70
+
71
+ .. code-block:: shell
72
+
73
+ pip3 install circuitpython-keymanager
74
+
75
+ To install system-wide (this may be required in some cases):
76
+
77
+ .. code-block:: shell
78
+
79
+ sudo pip3 install circuitpython-keymanager
80
+
81
+ To install in a virtual environment in your current project:
82
+
83
+ .. code-block:: shell
84
+
85
+ mkdir project-name && cd project-name
86
+ python3 -m venv .venv
87
+ source .env/bin/activate
88
+ pip3 install circuitpython-keymanager
89
+
90
+ Installing to a Connected CircuitPython Device with Circup
91
+ ==========================================================
92
+
93
+ Make sure that you have ``circup`` installed in your Python environment.
94
+ Install it with the following command if necessary:
95
+
96
+ .. code-block:: shell
97
+
98
+ pip3 install circup
99
+
100
+ With ``circup`` installed and your CircuitPython device connected use the
101
+ following command to install:
102
+
103
+ .. code-block:: shell
104
+
105
+ circup install relic_keymanager
106
+
107
+ Or the following command to update an existing version:
108
+
109
+ .. code-block:: shell
110
+
111
+ circup update
112
+
113
+ Usage Example
114
+ =============
115
+
116
+ .. code-block:: python
117
+
118
+ from relic_keymanager import Keyboard
119
+
120
+ keyboard = Keyboard()
121
+
122
+ keyboard.on_voice_press = lambda voice: print(f"Pressed: {voice.note.notenum:d}")
123
+ keyboard.on_voice_release = lambda voice: print(f"Released: {voice.note.notenum:d}")
124
+
125
+ for i in range(1, 4):
126
+ keyboard.append(i)
127
+ for i in range(3, 0, -1):
128
+ keyboard.remove(i)
129
+
130
+ Documentation
131
+ =============
132
+ API documentation for this library can be found on `Read the Docs <https://circuitpython-keymanager.readthedocs.io/>`_.
133
+
134
+ For information on building library documentation, please check out
135
+ `this guide <https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.
136
+
137
+ Contributing
138
+ ============
139
+
140
+ Contributions are welcome! Please read our `Code of Conduct
141
+ <https://github.com/relic-se/CircuitPython_KeyManager/blob/HEAD/CODE_OF_CONDUCT.md>`_
142
+ before contributing to help this project stay welcoming.
@@ -0,0 +1,6 @@
1
+ relic_keymanager.py,sha256=aaUIsdU-V3CLPS3eSek8OabbnfvCcEd24lh7mHnJYGw,35218
2
+ circuitpython_keymanager-1.1.1.dist-info/licenses/LICENSE,sha256=Kg8MNewibW0EZzuQ0LXwQuxM_Y_rvtyHAmUqqo-XIYw,1083
3
+ circuitpython_keymanager-1.1.1.dist-info/METADATA,sha256=7-ghbVcHhqd8BXSvKuLzoQQs9h6v_TqZpiV_GWCwz1k,4577
4
+ circuitpython_keymanager-1.1.1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
5
+ circuitpython_keymanager-1.1.1.dist-info/top_level.txt,sha256=CxXrg_QSCDRhgDhuhJEjYFyOq8lxfxnhpj4uI6J69jw,17
6
+ circuitpython_keymanager-1.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Cooper Dalrymple
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ relic_keymanager
relic_keymanager.py ADDED
@@ -0,0 +1,980 @@
1
+ # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
2
+ # SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple
3
+ #
4
+ # SPDX-License-Identifier: MIT
5
+ """
6
+ `relic_keymanager`
7
+ ================================================================================
8
+
9
+ Tools to manage notes in musical applications. Includes note priority, arpeggiation, and sequencing.
10
+
11
+
12
+ * Author(s): Cooper Dalrymple
13
+
14
+ Implementation Notes
15
+ --------------------
16
+
17
+ **Software and Dependencies:**
18
+
19
+ * Adafruit CircuitPython firmware for the supported boards:
20
+ https://circuitpython.org/downloads
21
+
22
+ * Adafruit's SimpleMath library: https://github.com/adafruit/Adafruit_CircuitPython_SimpleMath
23
+ """
24
+
25
+ # imports
26
+
27
+ __version__ = "1.1.1"
28
+ __repo__ = "https://github.com/relic-se/CircuitPython_KeyManager.git"
29
+
30
+ import asyncio
31
+ import random
32
+ import time
33
+
34
+ import keypad
35
+ from micropython import const
36
+
37
+ try:
38
+ from typing import Callable
39
+ except ImportError:
40
+ pass
41
+
42
+
43
+ class Note:
44
+ """Object which represents the parameters of a note. Contains note number, velocity, key number
45
+ (if evoked by a :class:`Key` object), and timestamp of when the note was created.
46
+
47
+ :param notenum: The MIDI note number representing the frequency of a note.
48
+ :param velocity: The strength of which a note was pressed from 0.0 to 1.0.
49
+ :param keynum: The index number of the :class:`Key` object which created this :class:`Note`
50
+ object.
51
+ """
52
+
53
+ def __init__(self, notenum: int, velocity: float = 1.0, keynum: int = None):
54
+ self.notenum = notenum
55
+ self.velocity = velocity
56
+ self.keynum = keynum
57
+ self.timestamp = time.monotonic()
58
+
59
+ notenum: int = None
60
+ """The MIDI note number representing the frequency of a note."""
61
+
62
+ velocity: float = 1.0
63
+ """The strength of which a note was pressed from 0.0 to 1.0."""
64
+
65
+ keynum: int = None
66
+ """The index number of the :class:`Key` object which created this :class:`Note` object."""
67
+
68
+ @property
69
+ def data(self) -> tuple[int, float, int]:
70
+ """Return all note data as tuple. The data is formatted as: (notenum:int, velocity:float,
71
+ keynum:int). Keynum may be set as `None` if not applicable.
72
+ """
73
+ return (self.notenum, self.velocity, self.keynum)
74
+
75
+ def __eq__(self, other):
76
+ if isinstance(other, self.__class__):
77
+ return self.notenum == other.notenum
78
+ elif isinstance(other, Voice):
79
+ return self.notenum == other.note.notenum if not other.note is None else False
80
+ elif type(other) == int:
81
+ return self.notenum == other
82
+ elif type(other) == list:
83
+ for i in other:
84
+ if self.__eq__(i):
85
+ return True
86
+ return False
87
+
88
+ def __ne__(self, other):
89
+ if isinstance(other, self.__class__):
90
+ return self.notenum != other.notenum
91
+ elif isinstance(other, Voice):
92
+ return self.notenum != other.note.notenum if not other.note is None else True
93
+ elif type(other) == int:
94
+ return self.notenum != other
95
+ elif type(other) == list:
96
+ for i in other:
97
+ if not self.__ne__(i):
98
+ return False
99
+ return True
100
+ else:
101
+ return False
102
+
103
+ def __lt__(self, other):
104
+ if isinstance(other, self.__class__):
105
+ return self.notenum < other.notenum
106
+ elif type(other) == int:
107
+ return self.notenum < other
108
+ else:
109
+ return False
110
+
111
+ def __gt__(self, other):
112
+ if isinstance(other, self.__class__):
113
+ return self.notenum > other.notenum
114
+ elif type(other) == int:
115
+ return self.notenum > other
116
+ else:
117
+ return False
118
+
119
+ def __le__(self, other):
120
+ if isinstance(other, self.__class__):
121
+ return self.notenum <= other.notenum
122
+ elif type(other) == int:
123
+ return self.notenum <= other
124
+ else:
125
+ return False
126
+
127
+ def __ge__(self, other):
128
+ if isinstance(other, self.__class__):
129
+ return self.notenum >= other.notenum
130
+ elif type(other) == int:
131
+ return self.notenum >= other
132
+ else:
133
+ return False
134
+
135
+
136
+ class Voice:
137
+ """Object which represents the parameters of a :class:`Keyboard` voice. Used to allocate
138
+ :class:`Note` objects to a pre-defined number of available slots in a logical manner based on
139
+ timing and keyboard mode.
140
+
141
+ :param index: The position of the voice in the pre-defined set of keyboard voices.
142
+ """
143
+
144
+ def __init__(self, index: int):
145
+ self.index = index
146
+ self.time = time.monotonic()
147
+
148
+ index: int = None
149
+ """The position of the voice in the pre-defined set of keyboard voices."""
150
+
151
+ time: float = None
152
+ """The last time in seconds at which a note was registered with this voice."""
153
+
154
+ _note: Note = None
155
+
156
+ @property
157
+ def note(self) -> Note:
158
+ """The :class:`Note` object assigned to this voice. When a note is assigned to a voice, the
159
+ voice is "active" until the note is cleared by setting it to `None`.
160
+ """
161
+ return self._note
162
+
163
+ @note.setter
164
+ def note(self, value: Note) -> None:
165
+ self._note = value
166
+ if not value is None:
167
+ self.time = time.monotonic()
168
+
169
+ @property
170
+ def active(self) -> bool:
171
+ """The active state of the voice. Will return `True` if a note has been assigned to this
172
+ voice.
173
+ """
174
+ return not self.note is None
175
+
176
+ def __eq__(self, other):
177
+ if isinstance(other, self.__class__):
178
+ return self.index == other.index
179
+ elif isinstance(other, Note) or type(other) == list:
180
+ return self.note == other
181
+ elif type(other) is int:
182
+ return self.index == other # NOTE: Use index or notenum?
183
+ else:
184
+ return False
185
+
186
+ def __ne__(self, other):
187
+ if isinstance(other, self.__class__):
188
+ return self.index != other.index
189
+ elif isinstance(other, Note) or type(other) == list:
190
+ return self.note != other
191
+ elif type(other) is int:
192
+ return self.index != other # NOTE: Use index or notenum?
193
+ else:
194
+ return False
195
+
196
+
197
+ class TimerStep:
198
+ """An enum-like class representing common step divisions."""
199
+
200
+ WHOLE: float = 0.25
201
+ """Whole note beat division"""
202
+
203
+ HALF: float = 0.5
204
+ """Half note beat division"""
205
+
206
+ QUARTER: float = 1.0
207
+ """Quarter note beat division"""
208
+
209
+ DOTTED_QUARTER: float = 1.5
210
+ """Dotted quarter note beat division"""
211
+
212
+ EIGHTH: float = 2.0
213
+ """Eighth note beat division"""
214
+
215
+ TRIPLET: float = 3.0
216
+ """Triplet note beat division"""
217
+
218
+ SIXTEENTH: float = 4.0
219
+ """Sixteenth note beat division"""
220
+
221
+ THIRTYSECOND: float = 8.0
222
+ """Thirtysecond note beat division"""
223
+
224
+
225
+ class Timer:
226
+ """An abstract class to help handle timing functionality of the :class:`Arpeggiator` and
227
+ :class:`Sequencer` classes. Note press and release timing is managed by bpm (beats per minute),
228
+ steps (divisions of a beat), and gate (note duration during step).
229
+
230
+ :param bpm: The beats per minute of timer.
231
+ :param steps: The number of steps to divide a single beat. The minimum value allowed is 0.25, or
232
+ a whole note.
233
+ :param gate: The duration of each pressed note per step to play before releasing as a ratio from
234
+ 0.0 to 1.0.
235
+ """
236
+
237
+ def __init__(self, bpm: float = 120.0, steps: float = TimerStep.EIGHTH, gate: float = 0.5):
238
+ self._reset(False)
239
+ self.gate = gate
240
+ self.bpm = bpm
241
+ self.steps = steps
242
+
243
+ def _update_timing(self) -> None:
244
+ self._step_time = 60.0 / self._bpm / self._steps
245
+ self._gate_duration = self._gate * self._step_time
246
+
247
+ def _reset(self, immediate=True):
248
+ self._now = time.monotonic()
249
+ if immediate:
250
+ self._now -= self._step_time
251
+
252
+ _bpm: float = 120.0
253
+
254
+ @property
255
+ def bpm(self) -> float:
256
+ """Beats per minute."""
257
+ return self._bpm
258
+
259
+ @bpm.setter
260
+ def bpm(self, value: float) -> None:
261
+ self._bpm = max(value, 1.0)
262
+ self._update_timing()
263
+
264
+ _steps: float = TimerStep.EIGHTH
265
+
266
+ @property
267
+ def steps(self) -> float:
268
+ """The number of steps per beat (or the beat division). The minimum value allowed is 0.25,
269
+ or a whole note. The pre-defined :class:`TimerStep` constants can be used here.
270
+ """
271
+ return self._steps
272
+
273
+ @steps.setter
274
+ def steps(self, value: float) -> None:
275
+ self._steps = max(value, TimerStep.WHOLE)
276
+ self._update_timing()
277
+
278
+ _gate: float = 0.5
279
+
280
+ @property
281
+ def gate(self) -> float:
282
+ """The duration each pressed note per step will play before releasing within a step of a
283
+ beat as a ratio of that step from 0.0 to 1.0.
284
+ """
285
+ return self._gate
286
+
287
+ @gate.setter
288
+ def gate(self, value: float) -> None:
289
+ self._gate = min(max(value, 0.0), 1.0)
290
+ self._update_timing()
291
+
292
+ _active: bool = False
293
+
294
+ @property
295
+ def active(self) -> bool:
296
+ """Whether or not the timer object is enabled (running)."""
297
+ return self._active
298
+
299
+ @active.setter
300
+ def active(self, value: bool) -> None:
301
+ if value == self._active:
302
+ return
303
+ self._active = value
304
+ if self._active:
305
+ self._now = time.monotonic() - self._step_time
306
+ else:
307
+ self._do_release()
308
+ if self.on_enabled:
309
+ self.on_enabled(self._active)
310
+
311
+ on_enabled: Callable[[bool], None] = None
312
+ """The callback method that is called when :attr:`active` is changed. Must have 1 parameter for
313
+ the current active state. Ie: :code:`def enabled(active):`
314
+ """
315
+
316
+ on_step: Callable[[], None] = None
317
+ """The callback method that is called when a step is triggered. This callback will fire whether
318
+ or not the step has pressed any notes. However, any pressed notes will occur before this
319
+ callback is called.
320
+ """
321
+
322
+ on_press: Callable[[int, float], None] = None
323
+ """The callback method that is called when a timed step note is pressed. Must have 2 parameters
324
+ for note value and velocity (0.0-1.0). Ie: :code:`def press(notenum, velocity):`.
325
+ """
326
+
327
+ on_release: Callable[[int], None] = None
328
+ """The callback method that is called when a timed step note is released. Must have 1 parameter
329
+ for note value. Velocity is always assumed to be 0.0. Ie: :code:`def release(notenum):`.
330
+ """
331
+
332
+ _last_press: list[int] = []
333
+
334
+ async def update(self):
335
+ """Update the timer object and call any relevant callbacks if a new beat step or the end of
336
+ the gate of a step is reached. The actual functionality of this method will depend on the
337
+ child class that utilizes the :class:`Timer` parent class.
338
+ """
339
+ while True:
340
+ if not self._active:
341
+ await self._sleep(0.01)
342
+ continue
343
+ self._update()
344
+ self._do_step()
345
+ if self._last_press:
346
+ await self._sleep(self._gate_duration)
347
+ self._do_release()
348
+ await self._sleep(self._step_time - self._gate_duration)
349
+ else:
350
+ await self._sleep(self._step_time)
351
+
352
+ async def _sleep(self, delay: float):
353
+ self._now += delay
354
+ await asyncio.sleep(self._now - time.monotonic())
355
+
356
+ def _update(self):
357
+ pass
358
+
359
+ def _do_step(self):
360
+ if callable(self.on_step):
361
+ self.on_step()
362
+
363
+ def _do_press(self, notenum, velocity):
364
+ if callable(self.on_press):
365
+ self.on_press(notenum, velocity)
366
+ self._last_press.append(notenum)
367
+
368
+ def _do_release(self):
369
+ if callable(self.on_release) and self._last_press:
370
+ for notenum in self._last_press:
371
+ self.on_release(notenum)
372
+ self._last_press.clear()
373
+
374
+
375
+ class ArpeggiatorMode:
376
+ """An enum-like class containing constaints for the possible modes of the :class:`Arpeggiator`
377
+ class.
378
+ """
379
+
380
+ UP: int = const(0)
381
+ """Play notes based on ascending note value."""
382
+
383
+ DOWN: int = const(1)
384
+ """Play notes based on descending note value."""
385
+
386
+ UPDOWN: int = const(2)
387
+ """Play notes based on note value in ascending order then descending order. The topmost and
388
+ bottommost notes will not be repeated.
389
+ """
390
+
391
+ DOWNUP: int = const(3)
392
+ """Play notes based on note value in descending order then ascending order. The topmost and
393
+ bottommost notes will not be repeated.
394
+ """
395
+
396
+ PLAYED: int = const(4)
397
+ """Play notes based on the time at which they were played (ascending)."""
398
+
399
+ RANDOM: int = const(5)
400
+ """Play notes in a random order."""
401
+
402
+
403
+ class Arpeggiator(Timer):
404
+ """Use this class to iterate over notes based on time parameters. Note press and release timing
405
+ is managed by bpm (beats per minute), steps (divisions of a beat), and gate (note duration
406
+ during step).
407
+
408
+ :param bpm: The beats per minute of timer.
409
+ :param steps: The number of steps to divide a single beat. The minimum value allowed is 0.25, or
410
+ a whole note.
411
+ :param gate: The duration of each pressed note per step to play before releasing as a ratio from
412
+ 0.0 to 1.0.
413
+ :param mode: The method of stepping through notes as specified by :class:`ArpeggiatorMode`
414
+ constants.
415
+ """
416
+
417
+ def __init__(
418
+ self, bpm: float = 120.0, steps: float = TimerStep.EIGHTH, mode: int = ArpeggiatorMode.UP
419
+ ):
420
+ Timer.__init__(
421
+ self,
422
+ bpm=bpm,
423
+ steps=steps,
424
+ )
425
+ self.mode = mode
426
+
427
+ _pos: int = 0
428
+
429
+ def _reset(self, immediate=True):
430
+ Timer._reset(self, immediate)
431
+ self._pos = 0
432
+
433
+ _octaves: int = 0
434
+
435
+ @property
436
+ def octaves(self) -> int:
437
+ """The number of octaves in which to extend the notes, either up or down."""
438
+ return self._octaves
439
+
440
+ @octaves.setter
441
+ def octaves(self, value: int) -> None:
442
+ self._octaves = value
443
+ if self._notes:
444
+ self.notes = self._raw_notes
445
+
446
+ _probability: float = 1.0
447
+
448
+ @property
449
+ def probability(self) -> float:
450
+ """The likeliness that a note will be played within a step, ranging from 0.0 (never) to 1.0
451
+ (always).
452
+ """
453
+ return self._probability
454
+
455
+ @probability.setter
456
+ def probability(self, value: float) -> None:
457
+ self._probability = min(max(value, 0.0), 1.0)
458
+
459
+ _mode: int = ArpeggiatorMode.UP
460
+
461
+ @property
462
+ def mode(self) -> int:
463
+ """The method of stepping through notes. See :class:`ArpeggiatorMode` for options."""
464
+ return self._mode
465
+
466
+ @mode.setter
467
+ def mode(self, value: int) -> None:
468
+ self._mode = value % 6
469
+ if self._notes:
470
+ self.notes = self._raw_notes
471
+
472
+ _raw_notes: list[Note] = []
473
+ _notes: list[Note] = []
474
+
475
+ def _get_notes(self, notes: list[Note] = []):
476
+ if not notes:
477
+ return notes
478
+
479
+ if abs(self._octaves) > 0:
480
+ l = len(notes)
481
+ for octave in range(1, abs(self._octaves) + 1):
482
+ for i in range(0, l):
483
+ notes.append(
484
+ Note(
485
+ notes[i].notenum + octave * (-1 if self._octaves < 0 else 1) * 12,
486
+ notes[i].velocity,
487
+ )
488
+ )
489
+
490
+ if self._mode == ArpeggiatorMode.UP:
491
+ notes.sort()
492
+ elif self._mode == ArpeggiatorMode.DOWN:
493
+ notes.sort(reverse=True)
494
+ elif self._mode == ArpeggiatorMode.UPDOWN:
495
+ notes.sort()
496
+ if len(notes) > 2:
497
+ _notes = notes[1:-1].copy()
498
+ _notes.reverse()
499
+ notes = notes + _notes
500
+ elif self._mode == ArpeggiatorMode.DOWNUP:
501
+ notes.sort(reverse=True)
502
+ if len(notes) > 2:
503
+ _notes = notes[1:-1].copy()
504
+ _notes.reverse()
505
+ notes = notes + _notes
506
+ # PLAYED = notes stay as is, RANDOM = index is randomized on update
507
+
508
+ return notes
509
+
510
+ @property
511
+ def notes(self) -> list[Note]:
512
+ """The :class:`Note` objects which the arpeggiator is currently stepping through ordered as
513
+ specified by :attr:`mode` and affected by :attr:`octaves`.
514
+ """
515
+ return self._notes
516
+
517
+ @notes.setter
518
+ def notes(self, value: list[Note]) -> None:
519
+ if not self._notes:
520
+ self._reset()
521
+ self._raw_notes = value.copy()
522
+ self._notes = self._get_notes(value)
523
+
524
+ def _update(self):
525
+ if self._notes:
526
+ if self._probability < 1.0 and (
527
+ self._probability == 0.0 or random.random() > self._probability
528
+ ):
529
+ return
530
+ if self.mode == ArpeggiatorMode.RANDOM:
531
+ self._pos = random.randrange(0, len(self._notes), 1)
532
+ else:
533
+ self._pos = (self._pos + 1) % len(self._notes)
534
+ self._do_press(self._notes[self._pos].notenum, self._notes[self._pos].velocity)
535
+
536
+
537
+ class Sequencer(Timer):
538
+ """Sequence notes using the :class:`Timer` class to create a multi-track note sequencer. By
539
+ default, the Sequencer is set up for a single 4/4 measure of 16 notes with one track. Each note
540
+ of each track can be assigned any note value and velocity. The length and number of tracks can
541
+ be reassigned during runtime.
542
+
543
+ :param length: The number of steps of each track. The minimum value allowed is 1.
544
+ :param tracks: The number of tracks to create and sequence. The minimum value allowed is 1.
545
+ :param bpm: The beats per minute of the timer.
546
+ """
547
+
548
+ def __init__(self, length: int = 16, tracks: int = 1, bpm: float = 120.0):
549
+ Timer.__init__(self, bpm=bpm, steps=TimerStep.SIXTEENTH)
550
+ self.length = length
551
+ self.tracks = tracks
552
+ self._data = [[None for j in range(self._length)] for i in range(self._tracks)]
553
+
554
+ _data: list = None
555
+
556
+ _length: int = 16
557
+
558
+ @property
559
+ def length(self) -> int:
560
+ """The number of steps for each track. If the length is shortened, all of the step data
561
+ beyond the new length will be deleted, and if the sequencer is also currently running, it
562
+ should loop back around automatically to the start of the track data. The minimum allowed
563
+ is 1.
564
+ """
565
+ return self._length
566
+
567
+ @length.setter
568
+ def length(self, value: int) -> None:
569
+ value = max(value, 1)
570
+ if self._data:
571
+ if value > self._length:
572
+ for i in range(self._tracks):
573
+ self._data[i] = self._data[i] + [None for j in range(value - self._length)]
574
+ elif value < self._length:
575
+ for i in range(self._tracks):
576
+ del self._data[i][value:]
577
+ self._length = value
578
+
579
+ _tracks: int = 1
580
+
581
+ @property
582
+ def tracks(self) -> int:
583
+ """The number of note tracks to sequence. If the number of tracks is shortened, the tracks
584
+ at an index greater to or equal than the number will be deleted. If a larger number of
585
+ tracks is provided, the newly created tracks will be empty. The minimum allowed is 1.
586
+ """
587
+ return self._tracks
588
+
589
+ @tracks.setter
590
+ def tracks(self, value: int) -> None:
591
+ value = max(value, 1)
592
+ if self._data:
593
+ if value > self._tracks:
594
+ self._data = self._data + [
595
+ [None for j in range(self._length)] for i in range(value - self._tracks)
596
+ ]
597
+ elif value < self._tracks:
598
+ del self._data[value:]
599
+ self._tracks = value
600
+
601
+ _pos: int = 0
602
+
603
+ @property
604
+ def position(self) -> int:
605
+ """The current position of the sequencer within the track length (0-based)."""
606
+ return self._pos
607
+
608
+ def set_note(self, position: int, notenum: int, velocity: float = 1.0, track: int = 0) -> None:
609
+ """Set the note value and velocity of a track at a specific step index.
610
+
611
+ :param position: Index of the step (0-based). Will be limited to the track length.
612
+ :param notenum: Value of the note.
613
+ :param velocity: Velocity of the note (0.0-1.0).
614
+ :param track: Index of the track (0-based). Will be limited to the track count.
615
+ """
616
+ track = min(max(track, 0), self._tracks)
617
+ position = min(max(position, 0), self._length)
618
+ self._data[track][position] = (notenum, velocity)
619
+
620
+ def get_note(self, position: int, track: int = 0) -> tuple[int, int]:
621
+ """Get the note data for a specified track and step position. If a note isn't defined at
622
+ specific index, a value of `None` will be returned.
623
+
624
+ :param position: Index of the step (0-based). Will be limited to the track length.
625
+ :param track: Index of the track (0-based). Will be limited to the track count.
626
+ :return: note data (notenum, velocity)
627
+ """
628
+ track = min(max(track, 0), self._tracks)
629
+ position = min(max(position, 0), self._length)
630
+ return self._data[track][position]
631
+
632
+ def has_note(self, position: int, track: int = 0) -> bool:
633
+ """Check whether or note a specific step within a track has been set with note data.
634
+
635
+ :param position: Index of the step (0-based). Will be limited to the track length.
636
+ :param track: Index of the track (0-based). Will be limited to the track count.
637
+ :return: if the track step has a note
638
+ """
639
+ return not self.get_note(position, track) is None
640
+
641
+ def remove_note(self, position: int, track: int = 0) -> None:
642
+ """Remove the note data as a specific step within a track.
643
+
644
+ :param position: Index of the step (0-based). Will be limited to the track length.
645
+ :param track: Index of the track (0-based). Will be limited to the track count.
646
+ """
647
+ track = min(max(track, 0), self._tracks)
648
+ position = min(max(position, 0), self._length)
649
+ self._data[track][position] = None
650
+
651
+ def get_track(self, track=0) -> list[tuple[int, int]]:
652
+ """Get list of note data for a specified track index (0-based). If the track isn't
653
+ available, a value of `None` will be returned.
654
+
655
+ :return: track data list of note tuples as (notenum, velocity)
656
+ """
657
+ return self._data[min(max(track, 0), self._tracks)]
658
+
659
+ on_step: Callable[[int], None] = None
660
+ """The callback method that is called when a step is triggered. This callback will fire whether
661
+ or not the step has any notes. However, any pressed notes will occur before this callback is
662
+ called. Must have 1 parameter for sequencer position index. Ie: :code:`def step(pos):`.
663
+ """
664
+
665
+ def _update(self):
666
+ self._pos = (self._pos + 1) % self._length
667
+ for i in range(self._tracks):
668
+ note = self._data[i][self._pos]
669
+ if note and note[0] > 0 and note[1] > 0:
670
+ self._do_press(note[0], note[1])
671
+
672
+ def _do_step(self):
673
+ if callable(self.on_step):
674
+ self.on_step(self._pos)
675
+
676
+
677
+ class KeyboardMode:
678
+ """An enum-like class representing Keyboard note handling modes."""
679
+
680
+ HIGH: int = const(0)
681
+ """When the keyboard is set as this mode, it will prioritize the highest note value."""
682
+
683
+ LOW: int = const(1)
684
+ """When the keyboard is set as this mode, it will prioritize the lowest note value."""
685
+
686
+ LAST: int = const(2)
687
+ """When the keyboard is set as this mode, it will prioritize notes by the order in when they
688
+ were played/appended.
689
+ """
690
+
691
+
692
+ class Keyboard:
693
+ """Manage notes, voice allocation, arpeggiator assignment, sustain, and relevant callbacks using
694
+ this class.
695
+
696
+ :param keys: A list of :class:`Key` objects which will be used to update the keyboard state.
697
+ :param max_voices: The maximum number of voices/notes to be played at once.
698
+ :param root: Set the base note number of the physical key inputs.
699
+ """
700
+
701
+ def __init__(
702
+ self,
703
+ keys: keypad = None,
704
+ max_voices: int = 1,
705
+ root: int = 48,
706
+ mode: int = KeyboardMode.HIGH,
707
+ ):
708
+ self.root = root
709
+ self._keys = keys
710
+ self.mode = mode
711
+ self.max_voices = max_voices
712
+ self._voices = [Voice(i) for i in range(self.max_voices)]
713
+
714
+ on_voice_press: Callable[[Voice], None] = None
715
+ """The callback method to be called when a voice is pressed. Must have 1 parameter for the
716
+ :class:`Voice` object. Ie: :code:`def press(voice):`.
717
+ """
718
+
719
+ on_voice_release: Callable[[Voice], None] = None
720
+ """The callback method to be called when a voice is released. Must have 1 parameter for the
721
+ :class:`Voice` object. Velocity is always assumed to be 0.0. Ie: :code:`def release(voice):`.
722
+ """
723
+
724
+ on_key_press: Callable[[int, int, float], None] = None
725
+ """The callback method to be called when a :class:`Key` object is pressed. Must have 3
726
+ parameters for keynum, note value, velocity (0.0-1.0), and keynum. Ie: :code:`def press(keynum,
727
+ notenum, velocity):`.
728
+ """
729
+
730
+ on_key_release: Callable[[int, int], None] = None
731
+ """The callback method to be called when a :class:`Key` object is released. Must have 2
732
+ parameters for keynum and note value. Velocity is always assumed to be 0.0. Ie: :code:`def
733
+ release(keynum, notenum):`.
734
+ """
735
+
736
+ _keys: keypad = None
737
+
738
+ @property
739
+ def keys(self) -> keypad:
740
+ """The :class:`keypad.Keys` object which will be used to update the keyboard state."""
741
+ return self._keys
742
+
743
+ _arpeggiator: Arpeggiator = None
744
+
745
+ @property
746
+ def arpeggiator(self) -> Arpeggiator:
747
+ """The :class:`Arpeggiator` object assigned to the keyboard."""
748
+ return self._arpeggiator
749
+
750
+ @arpeggiator.setter
751
+ def arpeggiator(self, value: Arpeggiator) -> None:
752
+ if self._arpeggiator:
753
+ self._arpeggiator.on_enabled = None
754
+ self._arpeggiator.on_press = None
755
+ self._arpeggiator.on_release = None
756
+ self._arpeggiator = value
757
+ self._arpeggiator.on_enabled = self._timer_enabled
758
+ self._arpeggiator.on_press = self._timer_press
759
+ self._arpeggiator.on_release = self._timer_release
760
+
761
+ _mode: int = KeyboardMode.HIGH
762
+
763
+ @property
764
+ def mode(self) -> int:
765
+ """The note allocation mode. Use one of the mode constants of :class:`KeyboardMode`. Note
766
+ allocation won't be updated until the next update call.
767
+ """
768
+ return self._mode
769
+
770
+ @mode.setter
771
+ def mode(self, value: int) -> None:
772
+ self._mode = value % 3
773
+
774
+ _sustain: bool = False
775
+ _sustained: list[Note] = []
776
+
777
+ @property
778
+ def sustain(self) -> bool:
779
+ """Whether or not the notes pressed are sustained after being released until this property
780
+ is set to `False`.
781
+ """
782
+ return self._sustain
783
+
784
+ @sustain.setter
785
+ def sustain(self, value: bool) -> None:
786
+ if value != self._sustain:
787
+ self._sustain = value
788
+ self._sustained = self._notes.copy() if self._sustain else []
789
+ self._update()
790
+
791
+ _notes: list[Note] = []
792
+
793
+ @property
794
+ def all_notes(self) -> list[Note]:
795
+ """All active :class:`Note` objects."""
796
+ return self._notes + self._sustained
797
+
798
+ @property
799
+ def notes(self) -> list[Note]:
800
+ """Active :class:`Notes` objects according to the current :class:`KeyboardMode`."""
801
+ notes = self.all_notes
802
+ if self._mode in {KeyboardMode.HIGH, KeyboardMode.LOW}:
803
+ notes.sort(reverse=(self._mode == KeyboardMode.HIGH))
804
+ else: # KeyboardMode.LAST
805
+ notes.sort(key=lambda note: note.timestamp)
806
+ return notes[: self._max_voices]
807
+
808
+ def append(self, notenum: int | Note, velocity: float = 1.0, keynum: int = None):
809
+ """Add a note to the keyboard buffer. Useful when working with MIDI input or another note
810
+ source. Any previous notes with the same notenum value will be removed automatically.
811
+
812
+ :param notenum: The number of the note. Can be defined by MIDI notes, a designated sample
813
+ index, etc. When using MODE_HIGH or MODE_LOW, the value of this parameter will affect
814
+ the order. A :class:`Note` object can be used instead of providing notenum, velocity,
815
+ and keynum parameters directly.
816
+ :param velocity: The velocity of the note from 0.0 through 1.0.
817
+ :param keynum: An additional index reference typically used to associate the note with a
818
+ physical :class:`Key` object. Not required for use of the keyboard.
819
+ """
820
+ self.remove(notenum, True)
821
+ note = notenum if isinstance(notenum, Note) else Note(notenum, velocity, keynum)
822
+ self._notes.append(note)
823
+ if self._sustain:
824
+ self._sustained.append(note)
825
+ self._update()
826
+
827
+ def remove(self, notenum: int | Note, remove_sustained: bool = False):
828
+ """Remove a note from the keyboard buffer. Useful when working with MIDI input or another
829
+ note source. If the note is found (and the keyboard isn't being sustained or
830
+ remove_sustained is set as `True`), the release callback will trigger automatically
831
+ regardless of the `update` parameter.
832
+
833
+ :param notenum: The value of the note that you would like to be removed. All notes in the
834
+ buffer with this value will be removed. Can be defined by MIDI note value, a designated
835
+ sample index, etc. Can also use a :class:`Note` object instead.
836
+ :param remove_sustained: Whether or not you would like to override the current sustained
837
+ state of the keyboard and release any notes that are being sustained.
838
+ """
839
+ if not notenum in self.all_notes:
840
+ return
841
+ self._notes = [note for note in self._notes if note != notenum]
842
+ if remove_sustained and self._sustain and self._sustained:
843
+ self._sustained = [note for note in self._sustained if note != notenum]
844
+ self._update()
845
+
846
+ async def update(self, delay: float = 0.01) -> None:
847
+ """Update :attr:`keys` objects if they were provided during initialization.
848
+
849
+ :param delay: The amount of time to sleep between polling in seconds.
850
+ """
851
+ while self._keys:
852
+ while True:
853
+ event = self._keys.events.get()
854
+ if not event:
855
+ break
856
+ notenum = self.root + event.key_number
857
+ if event.pressed:
858
+ self.append(notenum, keynum=event.key_number)
859
+ if callable(self.on_key_press):
860
+ self.on_key_press(event.key_number, notenum, 1.0)
861
+ elif event.released:
862
+ self.remove(notenum)
863
+ if callable(self.on_key_release):
864
+ self.on_key_release(event.key_number, notenum)
865
+ await asyncio.sleep(delay)
866
+
867
+ def _update(self) -> None:
868
+ if not self._arpeggiator or not self._arpeggiator.active:
869
+ self._update_voices(self.notes)
870
+ else:
871
+ self._arpeggiator.notes = self.all_notes
872
+
873
+ # Callbacks for arpeggiator
874
+ def _timer_enabled(self, active: bool) -> None:
875
+ if active:
876
+ self.arpeggiator.notes = self.all_notes
877
+ else:
878
+ self.update()
879
+
880
+ def _timer_press(self, notenum: int, velocity: float) -> None:
881
+ self._update_voices([Note(notenum, velocity)])
882
+
883
+ def _timer_release(self, notenum: int) -> None: # NOTE: notenum is ignored
884
+ self._update_voices()
885
+
886
+ _voices: list[Voice] = []
887
+
888
+ @property
889
+ def voices(self) -> list[Voice]:
890
+ """The :class:`Voice` objects used by the :class:`Keyboard` object."""
891
+ return self._voices
892
+
893
+ _max_voices: int = 1
894
+
895
+ @property
896
+ def max_voices(self) -> int:
897
+ """The maximum number of voices used by this keyboard to allocate notes. Must be greater
898
+ than 1. When this property is set, it will automatically release and delete any voices or
899
+ add new voice objects depending on the previous number of voices. Any voice related
900
+ callbacks may be triggered during this process.
901
+ """
902
+ return self._max_voices
903
+
904
+ @max_voices.setter
905
+ def max_voices(self, value: int) -> None:
906
+ self._max_voices = max(value, 1)
907
+ if len(self._voices) > self._max_voices:
908
+ for i in range(len(self._voices) - 1, self._max_voices - 1, -1):
909
+ self._release_voice(self._voices[i])
910
+ del self._voices[i]
911
+ elif len(self._voices) < self._max_voices:
912
+ for i in range(len(self._voices), self._max_voices):
913
+ self._voices.append(Voice(i))
914
+ self._update_voices()
915
+
916
+ @property
917
+ def active_voices(self) -> list[Voice]:
918
+ """All keyboard voices that are "active", have been assigned a note. The voices will
919
+ automatically be sorted by the time they were last assigned a note from oldest to newest.
920
+ """
921
+ voices = [voice for voice in self._voices if voice.active]
922
+ voices.sort(key=lambda voice: voice.time)
923
+ return voices
924
+
925
+ @property
926
+ def inactive_voices(self) -> list[Voice]:
927
+ """All keyboard voices that are "inactive", do not currently have a note assigned. The
928
+ voices will automatically be sorted by the time they were last assigned a note from oldest
929
+ to newest.
930
+ """
931
+ voices = [voice for voice in self._voices if not voice.active]
932
+ voices.sort(key=lambda voice: voice.time)
933
+ return voices
934
+
935
+ def _update_voices(self, notes: list[Note] = None) -> None:
936
+ # Release all active voices if no available notes
937
+ if notes is None or not notes:
938
+ if voices := self.active_voices:
939
+ for voice in voices:
940
+ self._release_voice(voice)
941
+ return
942
+
943
+ if voices := self.active_voices:
944
+ for voice in voices:
945
+ # Determine if voice has one of the notes in the buffer
946
+ has_note = False
947
+ for note in notes:
948
+ if voice.note is note:
949
+ has_note = True
950
+ break
951
+ if not has_note:
952
+ # Release voices without active notes
953
+ self._release_voice(voice)
954
+ else:
955
+ # Remove currently active notes from buffer
956
+ notes.remove(voice.note)
957
+
958
+ # Activate new notes
959
+ # If no voices are available, it will ignore remaining notes
960
+ if notes and (voices := self.inactive_voices):
961
+ voice_index = 0
962
+ for note in notes:
963
+ self._press_voice(voices[voice_index], note)
964
+ voice_index += 1
965
+ if voice_index >= len(voices):
966
+ break
967
+
968
+ def _press_voice(self, voice: Voice, note: Note) -> None:
969
+ voice.note = note
970
+ if callable(self.on_voice_press):
971
+ self.on_voice_press(voice)
972
+
973
+ def _release_voice(self, voice: Voice | list):
974
+ if type(voice) is list:
975
+ for i in voice:
976
+ self._release_voice(i)
977
+ elif voice.active:
978
+ if callable(self.on_voice_release):
979
+ self.on_voice_release(voice)
980
+ voice.note = None