-
Notifications
You must be signed in to change notification settings - Fork 0
/
piano_control.py
310 lines (264 loc) · 8.99 KB
/
piano_control.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
import os
import sys
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
import concurrent.futures
import time
from threading import Event
import keyboard
import mouse
import pygame
from pygame import midi
from packaging import Action, Keypress
from visuals import Display
# List of keyboard keybinds
KEYBOARD_KEYBINDS = {
"A#4": "w",
"A4": "a",
"B4": "s",
"C5": "d",
"D5": " ",
"F4": "ctrl",
"G4": "shift",
"G#4": "q",
"C#5": "e",
"E5": "f",
"F5": "1",
"F#5": "2",
"G5": "3",
"G#5": "4",
"A5": "5",
"A#5": "6",
"B5": "7",
"C6": "8",
"C#6": "9",
"D4": "F3",
"D#4": "tab",
"C#4": "esc",
}
# List of mouse click keybinds
MOUSE_KEYBINDS = {"G6": "left", "A6": "right", "G#6": "middle"}
# List of mouse direction keybinds
CURSOR_KEYBINDS = {
# Move left
"F6": (-1, 0),
# Move right
"C7": (1, 0),
# Move up
"A#6": (0, -1),
# Move down
"B6": (0, 1),
}
# Established "stop" key
STOP_KEY = "C8"
LETTERS = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
# Sets a sensitivity
SENSITIVITY = 10
# Sets modes
PIANO_MODE = False
KEY_LOG = True
# Global list of held buttons
held_directions = []
# Establishes clock
clock = pygame.time.Clock()
def read_input(midi_device: midi.Input) -> Keypress:
"""
Attemps to read from device. Returns a keypress.
"""
# If device returns a readable value
if midi_device.poll():
# Accesses event from device in format:
# [[status,data1,data2,data3],timestamp]
event: list[list[int], int] = midi_device.read(1)[0] # type: ignore
# Accesses data from event
data = event[0]
# If existant data:
if data:
# Returns a populated keypress
return Keypress(
letter=LETTERS[data[1] % 12],
octave=data[1] // 12,
velocity=data[2],
timestamp=event[1], # type: ignore
)
# If any step fails, returns an empty keypress
return Keypress()
def process_keypress(keypress: Keypress, display: Display) -> bool:
"""
Determines if a keypress should activate controls and display. Returns
true if stopping.
"""
# If empty action, returns false
if keypress.is_empty():
return False
# Updates display
display.update_display(keypress)
# If key log is active, prints keypress
if KEY_LOG:
print(keypress.to_string())
# If in piano mode, returns false
if PIANO_MODE:
return False
# If in list, presses corresponding keyboard button
if keypress.note in KEYBOARD_KEYBINDS:
print("toggling keyboard")
toggle_device(keypress, keyboard)
# If in list, presses corresponding mouse button
elif keypress.note in MOUSE_KEYBINDS:
toggle_device(keypress, mouse)
# If mouse direction, add/remove to/from queue
elif keypress.note in CURSOR_KEYBINDS:
update_held_queue(keypress)
# Exits program if stop key is pressed
return keypress.note == STOP_KEY
def toggle_device(keypress: Keypress, device) -> None:
"""
Toggles device based on keypress.
"""
# Translates note into hotkey
hotkey = {**KEYBOARD_KEYBINDS, **MOUSE_KEYBINDS}[keypress.note]
# Presses device hotkey
if keypress.action == Action.PRESS:
device.press(hotkey)
# Releases device hotkey
elif keypress.action == Action.RELEASE:
device.release(hotkey)
def update_held_queue(keypress: Keypress) -> None:
"""
Updates held queue with current keypress.
"""
# Translates direction from keypress
direction = CURSOR_KEYBINDS[keypress.note]
# If a keypress PRESS action and direction not already in queue
if keypress.action == Action.PRESS and direction not in held_directions:
# Add direction to queue
held_directions.append(direction)
# If a keypress RELEASE action and direction is in queue
elif keypress.action == Action.RELEASE and direction in held_directions:
# Remove direction from queue
held_directions.remove(direction)
def input_loop(midi_device: midi.Input, stop_event: Event) -> None:
"""
Acquires button input continuously from device. Returns true if stopping.
"""
# Creates a display object
display = Display()
# While stop event is npt set:
while not stop_event.is_set():
# Limits queries to speicifed Hz
clock.tick(240)
# Updates window and checks for closing
for event in pygame.event.get():
if event.type == pygame.QUIT:
print("Close window.")
return
# Reads input into keypress
keypress = read_input(midi_device)
# Processes button press and stops if specified
if process_keypress(keypress, display):
return
def cursor_loop(stop_event: Event) -> None:
"""
Translates directions in held list into mouse movements.
"""
# While stop event is not set:
while not stop_event.is_set():
# Limits queries to specified Hz
clock.tick(60)
# For every held direction:
for direction in held_directions:
# If there is no directly opposing direction:
if (-direction[0], -direction[1]) not in held_directions:
# Unpacks and scales x and y from direction tuple
x, y = direction[0] * SENSITIVITY, direction[1] * SENSITIVITY
# Moves mouse in direction
mouse.move(x, y, False, 0)
def init_midi_devices() -> midi.Input:
"""
Initializes and lists MIDI devices. Returns first found input device.
"""
# Intializes MIDI
midi.init()
# Prints all devices
print("\nDEVICES:")
# For a range of all MIDI devices:
for device_index in range(midi.get_count()):
# Unpacks info
device_name, device_input = device_info(device_index)
# Prints device info
print(f"[{device_index}] {device_name:40}", end="Type: ")
# Determines if input or output device
if device_input:
print("INPUT")
else:
print("OUTPUT")
# Gets ID of first input device
input_index = midi.get_default_input_id()
# If no device, raises error
if input_index == -1:
raise IndexError("No input device detected")
# Prints device info
print(f"\nStarting input using device: {device_info(input_index)[0]}\n")
# Returns device
return midi.Input(input_index)
def device_info(device_index: int) -> tuple[str, bool]:
"""
Accesses device information. Returns tuple of name and state.
"""
# Get info
info: tuple[str, str, int, int, int] = midi.get_device_info(device_index)
# Returns name and input state
return (info[1].decode("UTF-8"), bool(info[2])) # type:ignore
def main() -> None:
"""
Runs main controller actions.
"""
# Initializes midi devices and assigns input device
midi_device = init_midi_devices()
time.sleep(0.5)
# Creates an event to shut down all running tasks
stop_event = Event()
# Opens a threading executor
with concurrent.futures.ThreadPoolExecutor() as executor:
# Opens thread for button input
input_future = executor.submit(
# Feeds multiple arguments into loop inside submit function
lambda p: input_loop(*p),
[midi_device, stop_event],
)
# Opens thread for directional processing
cursor_future = executor.submit(cursor_loop, stop_event)
# for future in concurrent.futures.as_completed([input_future, mouse_future]):
# print(repr(future.exception()))
try:
# While stop event has not been set:
while not stop_event.is_set():
time.sleep(0.2)
# If either thread is done, stop all threads:
if input_future.done():
print("Input future done.")
stop_event.set()
if cursor_future.done():
print("Cursor future done.")
stop_event.set()
# Catch keyboard interrupt exception
except KeyboardInterrupt:
stop_event.set()
print("Keyboard interrupt.")
if __name__ == "__main__":
run = True
# Argument handling
if "--help" in sys.argv:
run = False
print("\nUse -p for piano mode (no controls) or -nl to turn off keylog.\n")
# Enters piano mode
if "-p" in sys.argv:
PIANO_MODE = True
print("\nStarting in PIANO MODE. . .")
# Disables key log
if "-nl" in sys.argv:
KEY_LOG = False
if run:
# Runs program
main()
# Ends program
print("\nThanks for using the keyboard swap!\n")