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.
- circuitpython_keymanager-1.1.1.dist-info/METADATA +142 -0
- circuitpython_keymanager-1.1.1.dist-info/RECORD +6 -0
- circuitpython_keymanager-1.1.1.dist-info/WHEEL +5 -0
- circuitpython_keymanager-1.1.1.dist-info/licenses/LICENSE +21 -0
- circuitpython_keymanager-1.1.1.dist-info/top_level.txt +1 -0
- relic_keymanager.py +980 -0
|
@@ -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,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
|