-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathInstrumentUI.py
More file actions
203 lines (171 loc) · 7.21 KB
/
Copy pathInstrumentUI.py
File metadata and controls
203 lines (171 loc) · 7.21 KB
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
# pyright: reportUnknownVariableType=false, reportUnknownParameterType=false, reportUnknownArgumentType=false, reportUnknownMemberType=false, reportUnannotatedClassAttribute=false, reportMissingParameterType=false, reportMissingImports=false, reportAttributeAccessIssue=false
import asyncio
import time
from typing import Any, Callable
from .MusicEvent import MusicEvent, NullEvent, Note
from .Focusable import Focusable
from .ButtonEvent import ButtonEvent, DOWN, UP
from .Instrument import Instrument
from .PurplePattern import PurpleRandom
from .Comms import Comms
from system.eventbus import eventbus
from system.patterndisplay.events import PatternSet
from events.input import Buttons, ButtonDownEvent, ButtonUpEvent
from app_components import set_color
class InstrumentUI(Focusable):
BUTTON_VALUES = {
"A": Note(0),
"B": Note(1),
"C": Note(2),
"D": Note(3),
"E": Note(4),
"F": Note(5),
# 2026 frontboard touchpads
"TOUCH01": Note(0),
"TOUCH02": Note(1),
"TOUCH03": Note(2),
"TOUCH04": Note(3),
"TOUCH05": Note(4),
"TOUCH06": Note(5),
"TOUCH07": Note(6),
"TOUCH08": Note(7),
"TOUCH09": Note(8),
"TOUCH10": Note(9),
"TOUCH11": Note(10),
"TOUCH12": Note(11),
}
bridgeMAC: str | None = None
instrument: Instrument
_comms: Comms
_notesOffset: int
darkPurple = (0.2, 0.0, 0.2)
purpleText = (0.6, 0.4, 0.6)
lavenderText = (1.0, 0.8, 1.0)
# Keepalive tracking
_held_notes: dict[str, tuple[int, int]] = {} # button_name -> (channel, note)
_last_keepalive_time: int = 0
_keepalive_interval_ms: int = 50 # Send keepalive every 50ms
def __init__(
self,
instrument: Instrument,
onMusicEvent: Callable[[MusicEvent, ButtonEvent], None],
comms: Comms,
notesOffset: int,
) -> None:
super().__init__()
self.held_buttons: set[Any] = set()
self._held_notes = {} # Initialize held notes tracking
self.instrument = instrument
self._onMusicEvent = onMusicEvent
self._comms = comms
self._notesOffset = notesOffset
self._last_keepalive_time = 0
eventbus.emit(PurpleRandom)
def _note_event_for_button(self, button_name: str) -> MusicEvent:
event = self.BUTTON_VALUES.get(button_name)
if event is None:
return NullEvent()
return event
def handleButton(
self, event: ButtonDownEvent, buttonEventType: ButtonEvent
) -> None:
button_name = event.button.name
if button_name not in self.BUTTON_VALUES:
print(f"Unknown button pressed: {button_name}")
return
if event.button.group == "TwentyTwentySix":
# Handle as control button as only the TOUCH pads are notes on 2026 frontboard
print(f"Control button pressed: {button_name}")
if buttonEventType == DOWN:
if button_name in self.held_buttons:
return
self.held_buttons.add(button_name)
# Track the note value for this button (for keepalive purposes)
music_event = self._note_event_for_button(button_name)
if isinstance(music_event, Note):
midi_note = music_event.value + self._notesOffset
self._held_notes[button_name] = (self.instrument.midiChannel, midi_note)
self._onMusicEvent(music_event, DOWN)
elif buttonEventType == UP:
self.held_buttons.discard(button_name)
self._held_notes.pop(button_name, None) # Remove from tracking
self._onMusicEvent(self._note_event_for_button(button_name), UP)
def draw(self, ctx) -> None:
yOffset = 48
lineHeight = 22
# background colour
set_color(ctx, self.darkPurple)
ctx.rectangle(-120, -120, 240, 240).fill()
ctx.font = "sans-serif"
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
bridge_connected = self.bridgeMAC is not None
ctx.font_size = 20
if bridge_connected:
# ctx.rgb(0, 0.5, 0).move_to(0, -70).text(f"connected: {self.bridgeMAC}")
ctx.rgb(self.purpleText[0], self.purpleText[1], self.purpleText[2])
ctx.move_to(0, -yOffset).text("you are")
ctx.font_size = 40
# Calculate width at desired font size
width = ctx.text_width(self.instrument.name)
max_width = 230
# Scale font down if text is too wide
if width > max_width:
ctx.font_size = 40 * max_width / width
# round to nearest 0.125 for consistency
ctx.font_size = int(ctx.font_size * 8) / 8
ctx.rgb(self.lavenderText[0], self.lavenderText[1], self.lavenderText[2])
ctx.move_to(0, 0).text(self.instrument.name)
ctx.rgb(self.purpleText[0], self.purpleText[1], self.purpleText[2])
ctx.font_size = 20
if self.instrument.hint is not None:
for i, line in enumerate(self.instrument.hint.splitlines()):
ctx.move_to(0, yOffset + ((i - 1) * lineHeight)).text(line)
elif (
self.instrument.shouldPitchBend == True
and self.instrument.shouldModulate == True
):
ctx.move_to(0, yOffset).text("tilt to pitch bend")
ctx.move_to(0, yOffset + 18).text("and modulate!")
elif self.instrument.shouldPitchBend == True:
ctx.move_to(0, yOffset).text("tilt to pitch bend!")
elif self.instrument.shouldModulate == True:
ctx.move_to(0, yOffset).text("tilt to modulate!")
else:
ctx.move_to(0, yOffset).text("press some buttons!")
else:
ctx.font_size = 20
ctx.rgb(0.8, 0.0, 0.1).move_to(0, -(yOffset + lineHeight)).text(
"not connected."
)
ctx.rgb(
self.purpleText[0],
self.purpleText[1],
self.purpleText[2],
).move_to(0, -yOffset * 0.8).text("go to the")
ctx.font_size = 50
ctx.rgb(
self.lavenderText[0],
self.lavenderText[1],
self.lavenderText[2],
).move_to(0, 0).text("MusicJam")
ctx.font_size = 20
ctx.rgb(
self.purpleText[0],
self.purpleText[1],
self.purpleText[2],
).move_to(0, yOffset * 0.8).text("installation to play!")
# ctx.move_to(0, yOffset * 0.8 + lineHeight).text("to play!")
def update(self, delta: int) -> bool:
# Send keepalives for held notes at regular intervals
current_time = time.ticks_ms()
if self._held_notes and (
current_time - self._last_keepalive_time >= self._keepalive_interval_ms
):
loop = asyncio.get_event_loop()
for channel, note in self._held_notes.values():
loop.create_task(self._comms.sendNoteKeepalive(channel, note))
self._last_keepalive_time = current_time
return True
def setBridgeMAC(self, mac: str) -> None:
self.bridgeMAC = mac