-
Notifications
You must be signed in to change notification settings - Fork 82
/
LoopSelectorComponent.py
313 lines (275 loc) · 13.4 KB
/
LoopSelectorComponent.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
311
312
313
import time
from _Framework.ButtonElement import ButtonElement
from _Framework.ControlSurfaceComponent import ControlSurfaceComponent
STEPSEQ_MODE_MULTINOTE = 2
class LoopSelectorComponent(ControlSurfaceComponent):
def __init__(self, step_sequencer, buttons, control_surface):
ControlSurfaceComponent.__init__(self)
self._control_surface = control_surface
self.set_enabled(False)
self._step_sequencer = step_sequencer
self._clip = None # clip being played
self._notes = None # notes of the clip
self._playhead = None # contains the clip playing position
self._loop_end = 0
self._loop_start = 0
self._blocksize = 8 # number of notes per block -> how many steps are in a button (depending on quantization for note length variable)
self._block = 0 # currently selected block (button)
self._force = True # used to force a state change / message send
# used for loop selection
self._last_button_idx = -1
self._last_button_time = time.time()
self._loop_point1 = -1
self._loop_point2 = -1
self._cache = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1] # Length=16
self._buttons = buttons
for button in self._buttons: # iterate 16 buttons of 4x4 lower right matrix section
assert isinstance(button, ButtonElement)
button.remove_value_listener(self._loop_button_value)
button.add_value_listener(self._loop_button_value,
identify_sender=True)
def disconnect(self):
self._top_buttons = None
@property
def _number_of_lines_per_note(self):
if self._mode == STEPSEQ_MODE_MULTINOTE:
return self._step_sequencer._number_of_lines_per_note
else:
return 1
def set_clip(self, clip):
self._clip = clip
@property
def _mode(self):
return self._step_sequencer._mode
def set_note_cache(self, note_cache):
self._note_cache = note_cache
def set_playhead(self, playhead, updateBlock=False):
self._playhead = playhead
if updateBlock and self._playhead is not None:
self._block = int(self._playhead / self._blocksize / self._quantization)
self._step_sequencer.set_page(self._block)
self.update()
@property
def _is_mute_shifted(self):
return self._step_sequencer._is_mute_shifted
@property
def _is_velocity_shifted(self):
return self._step_sequencer._note_editor._is_velocity_shifted
@property
def _quantization(self):
return self._step_sequencer._quantization
@property
def block(self):
return self._block
def set_blocksize(self, blocksize):
self._blocksize = blocksize
def set_enabled(self, enabled):
self._force = True
ControlSurfaceComponent.set_enabled(self, enabled)
# Read Live's Clip loop values to LoopSelector Values OK
def _get_clip_loop(self):
if self._clip != None:
self._loop_start = self._clip.loop_start
self._loop_end = self._clip.loop_end
else:
self._loop_start = 0
self._loop_end = 0
# Write LoopSelector Values to Live's Clip loop values (loop and marker) OK
def set_clip_loop(self, start, end):
if self._clip != None:
self._loop_end = end
self._loop_start = start
if self._loop_start >= self._clip.loop_end:
self._clip.loop_end = self._loop_end
self._clip.loop_start = self._loop_start
self._clip.end_marker = self._loop_end
self._clip.start_marker = self._loop_start
else:
self._clip.loop_start = self._loop_start
self._clip.loop_end = self._loop_end
self._clip.start_marker = self._loop_start
self._clip.end_marker = self._loop_end
self.update()
# LoopSelector listener OK
def _loop_button_value(self, value, sender):
# Allows to make selection by hold and pressing marker buttons
# Selects simple page by double click on region button
# Allows to mute and delete notes in a range
# Allows to duplicate a range forwards (to empty regions)
if self.is_enabled():
idx = self._buttons.index(sender)
if value > 0: # This allow to setup loop range by pressing two buttons at a time [Start,End]
if self._loop_point1 == -1:
self._loop_point1 = idx
elif self._loop_point2 == -1:
self._loop_point2 = idx
# Button released
elif self._loop_point1 != -1:
setloop = self._loop_point2 != -1 # two buttons pressed
if self._loop_point2 == -1:
self._loop_point2 = idx # _loop_point1 = _loop_point2
if self._last_button_idx == idx and (
time.time() - self._last_button_time) < 0.25: # Double clic set loop subsection (depending on quantization)
setloop = True
self._last_button_time = time.time()
self._last_button_idx = -1
if self._loop_point1 != -1 and self._loop_point2 != -1:
start = min(self._loop_point1, self._loop_point2)
end = max(self._loop_point1, self._loop_point2) + 1
self._block = start
if setloop:
if self._is_mute_shifted:
if self._is_velocity_shifted:
self._mute_notes_in_range(
start * self._blocksize * self._quantization,
end * self._blocksize * self._quantization)
else:
self._delete_notes_in_range(
start * self._blocksize * self._quantization,
end * self._blocksize * self._quantization)
else:
if self._is_velocity_shifted: # FIX, see if can copy backwards
self._extend_clip_content(
start * self._blocksize * self._quantization,
self._loop_end,
end * self._blocksize * self._quantization)
self.set_clip_loop(
start * self._blocksize * self._quantization,
end * self._blocksize * self._quantization)
self._step_sequencer.set_page(
self._block) # set sequencer focus
self._loop_point1 = -1
self._loop_point2 = -1
self.update()
self._last_button_time = time.time()
self._last_button_idx = idx
# Index check for page boundaries scroll OK
def can_scroll(self, blocks):
if self._clip != None:
if (blocks + self._block) < 0:
return False
if (
blocks + self._block) * 8 * self._quantization * self._number_of_lines_per_note < self._clip.loop_start:
return False
if (
blocks + self._block + 1) * 8 * self._quantization * self._number_of_lines_per_note > self._clip.loop_end:
return False
return True
return False
# Does the actual scroll OK
def scroll(self, blocks):
if self._clip != None and self.can_scroll(blocks):
self._block = blocks + self._block
self._step_sequencer.set_page(self._block)
# Iterates refreshing all loop selector buttons (called from playing position listener) OK
def update(self):
if self.is_enabled():
self._get_clip_loop() # gets the loop start/end values from the clip -> self._loop_start & self._loop_end
i = 0
for button in self._buttons: # iterate 16 buttons of 4x4 lower right matrix section
if self._clip == None: # Disable/turn off all buttons
button.set_on_off_values("DefaultButton.Disabled",
"DefaultButton.Disabled")
if self._cache[i] != button._off_value:
button.turn_off()
self._cache[i] = button._off_value
else:
# is the button in loop range
in_loop = (
i * self._blocksize * self._quantization < self._loop_end) and (
i * self._blocksize * self._quantization >= self._loop_start)
# is the playing position is inside the block represented by the button
playing = self._playhead != None and self._playhead >= i * self._blocksize * self._quantization and self._playhead < (
i + 1) * self._blocksize * self._quantization
# is this block selected (green)
selected = i == self.block
if in_loop:
if playing:
if selected:
self._cache[
i] = "StepSequencer.LoopSelector.SelectedPlaying"
else:
self._cache[
i] = "StepSequencer.LoopSelector.Playing"
else:
if selected:
self._cache[
i] = "StepSequencer.LoopSelector.Selected"
else:
self._cache[
i] = "StepSequencer.LoopSelector.InLoop"
else:
if playing:
if selected:
self._cache[
i] = "StepSequencer.LoopSelector.SelectedPlaying"
else:
self._cache[
i] = "StepSequencer.LoopSelector.Playing"
else:
if selected:
self._cache[
i] = "StepSequencer.LoopSelector.Selected"
else:
self._cache[i] = "DefaultButton.Disabled"
if self._cache[
i] != button._on_value or self._force: # Enable/turn on all buttons
button.set_on_off_values(self._cache[i], self._cache[i])
button.turn_on()
i = i + 1
self._force = False
# Make a copy of the current loop to the next N empty blocks OK
def _extend_clip_content(self, loop_start, old_loop_end, new_loop_end):
if (self._no_notes_in_range(old_loop_end, new_loop_end, True)):
clip_looping_length = 0
if (old_loop_end > 1):
power = 1
while (power * 2 < old_loop_end):
power *= 2
clip_looping_length = (power)
clone_length = new_loop_end - old_loop_end
if (clip_looping_length > 0):
clone_start_point = (old_loop_end % clip_looping_length)
else:
clone_start_point = 0
self._copy_notes_in_range(clone_start_point,
clone_start_point + clone_length,
old_loop_end)
# Does the note by note copy OK
def _copy_notes_in_range(self, start, end, new_start):
new_notes = list(self._note_cache)
# for i in range()
for note in new_notes:
if note[1] >= start and note[1] < end:
new_notes.append(
[note[0], note[1] + new_start - start, note[2], note[3],
note[4]])
self._clip.select_all_notes()
self._clip.replace_selected_notes(tuple(new_notes))
# Checks if a range is empty OK
def _no_notes_in_range(self, start, end, or_after):
for note in list(self._note_cache):
if note[1] >= start and (note[1] < end or or_after):
return (False)
return (True)
# Deletes a block of notes OK
def _delete_notes_in_range(self, start, end):
new_notes = list()
for note in list(self._note_cache):
if note[1] < start or note[1] >= end:
new_notes.append(note)
self._clip.select_all_notes()
self._clip.replace_selected_notes(tuple(new_notes))
# Mutes a block of notes OK
def _mute_notes_in_range(self, start, end):
new_notes = list()
for note in list(
self._note_cache): # Note -> tuple containing pitch, time, duration, velocity, and mute
if note[1] < start or note[1] >= end: # Note time
new_notes.append(note)
else:
new_notes.append([note[0], note[1], note[2], note[3],
not note[4]]) # Negate mute state
self._clip.select_all_notes()
self._clip.replace_selected_notes(tuple(new_notes))