1
2 from __future__ import with_statement
3 from functools import partial, wraps
4 from itertools import chain
5 from contextlib import contextmanager
6 import Live
7 from Dependency import inject
8 from Util import BooleanContext, first, find_if, const, in_range
9 from Debug import debug_print
10 from SubjectSlot import SlotManager
11 from DeviceComponent import DeviceComponent
12 from PhysicalDisplayElement import PhysicalDisplayElement
13 from InputControlElement import InputControlElement, MIDI_CC_TYPE, MIDI_PB_TYPE, MIDI_NOTE_TYPE, MIDI_SYSEX_TYPE, MIDI_PB_STATUS
14 import Task
15 import Defaults
16 CS_LIST_KEY = 'control_surfaces'
19 """
20 This class is not to be instantiated. We just use it to check
21 wether the module have been unloaded (showing leaking listeners).
22 """
23 pass
24
27 """
28 Methods from two control surfaces that use the component_guard can
29 not live in the stack at the same time. Use this for methods that
30 can be called implicitly by other control surfaces to delay
31 execution.
32 """
33
34 @wraps(method)
35 def wrapper(self, *a, **k):
36
37 def doit():
38 return method(self, *a, **k)
39
40 self.schedule_message(1, doit)
41
42 return wrapper
43
46 """
47 Central base class for scripts based on the new Framework. New
48 scripts need to subclass this class and add special behavior.
49 """
50
51 - def __init__(self, c_instance, publish_self = True, *a, **k):
52 """ Define and Initialize standard behavior """
53 super(ControlSurface, self).__init__(*a, **k)
54 self.canonical_parent = None
55 if publish_self:
56 if isinstance(__builtins__, dict):
57 if CS_LIST_KEY not in __builtins__.keys():
58 __builtins__[CS_LIST_KEY] = []
59 __builtins__[CS_LIST_KEY].append(self)
60 else:
61 if not hasattr(__builtins__, CS_LIST_KEY):
62 setattr(__builtins__, CS_LIST_KEY, [])
63 cs_list = getattr(__builtins__, CS_LIST_KEY)
64 cs_list.append(self)
65 setattr(__builtins__, CS_LIST_KEY, cs_list)
66 self._c_instance = c_instance
67 self._pad_translations = None
68 self._suggested_input_port = str('')
69 self._suggested_output_port = str('')
70 self._components = []
71 self._displays = []
72 self.controls = []
73 self._highlighting_session_component = None
74 self._device_component = None
75 self._device_selection_follows_track_selection = False
76 self._forwarding_long_identifier_registry = {}
77 self._forwarding_registry = {}
78 self._is_sending_scheduled_messages = BooleanContext()
79 self._remaining_scheduled_messages = []
80 self._task_group = Task.TaskGroup(auto_kill=False)
81 self._in_build_midi_map = BooleanContext()
82 self._suppress_requests_counter = 0
83 self._rebuild_requests_during_suppression = 0
84 self._enabled = True
85 self._in_component_guard = BooleanContext()
86 self._accumulate_midi_messages = BooleanContext()
87 self._midi_message_dict = {}
88 self._midi_message_list = []
89 self._midi_message_count = 0
90 self._control_surface_injector = inject(parent_task_group=const(self._task_group), show_message=const(self.show_message), register_component=const(self._register_component), register_control=const(self._register_control), request_rebuild_midi_map=const(self.request_rebuild_midi_map), send_midi=const(self._send_midi), song=self.song).everywhere()
91 with self.setting_listener_caller():
92 self.song().add_visible_tracks_listener(self._on_track_list_changed)
93 self.song().add_scenes_listener(self._on_scene_list_changed)
94 self.song().view.add_selected_track_listener(self._on_selected_track_changed)
95 self.song().view.add_selected_scene_listener(self._on_selected_scene_changed)
96
97 @property
99 return tuple(filter(lambda comp: not comp.is_private, self._components))
100
102 """ Acceptance tests should call this function before using the script,
103 to ensure an appropriate testing state """
104 pass
105
107 return self._task_group
108
109 _tasks = property(_get_tasks)
110
112 """ Returns a reference to the application that we are running in """
113 return Live.Application.get_application()
114
116 """ Returns a reference to the Live song instance that we control """
117 return self._c_instance.song()
118
148
150 """ Returns list of registered control surfaces """
151 control_surfaces = []
152 if isinstance(__builtins__, dict):
153 if CS_LIST_KEY in __builtins__.keys():
154 control_surfaces = __builtins__[CS_LIST_KEY]
155 elif hasattr(__builtins__, CS_LIST_KEY):
156 control_surfaces = getattr(__builtins__, CS_LIST_KEY)
157 return control_surfaces
158
160 """
161 Live -> Script
162 """
163 return self._device_component != None
164
165 @_scheduled_method
167 """
168 Live -> Script
169 Live tells the script which device to control
170 """
171 raise self._device_component != None or AssertionError
172 with self.component_guard():
173 self._device_component.set_lock_to_device(True, device)
174
175 @_scheduled_method
177 """
178 Live -> Script
179 Live tells the script to unlock from a certain device
180 """
181 raise self._device_component != None or AssertionError
182 with self.component_guard():
183 self._device_component.set_lock_to_device(False, device)
184
185 @_scheduled_method
187 """
188 Live -> Script
189 Live tells the script which bank to use.
190 """
191 raise self._device_component != None or AssertionError
192 with self.component_guard():
193 self._device_component.restore_bank(bank_index)
194
195 @_scheduled_method
197 """
198 Live -> Script
199 Live tells the script to unlock from a certain device
200 """
201 with self.component_guard():
202 self._device_component.set_device(device)
203
208
210 """ Live -> Script: Live can ask for the name of the script's
211 prefered output port"""
212 return self._suggested_output_port
213
226
238
240 return self._pad_translations != None
241
243 raise self._highlighting_session_component is None or AssertionError, 'There must be one session component only'
244 self._highlighting_session_component = session_component
245 self._highlighting_session_component.set_highlighting_callback(self._set_session_highlight)
246
248 """ Return the session component showing the ring in Live session """
249 return self._highlighting_session_component
250
252 """ Displays the given message in Live's status bar """
253 raise isinstance(message, (str, unicode)) or AssertionError
254 self._c_instance.show_message(message)
255
257 """ Writes the given message into Live's main log file """
258 message = '(%s) %s' % (self.__class__.__name__, ' '.join(map(str, message)))
259 console_message = 'LOG: ' + message
260 if debug_print != None:
261 debug_print(console_message)
262 else:
263 print console_message
264 if self._c_instance:
265 self._c_instance.log_message(message)
266
269
271 """ Called by the Application as soon as all scripts are initialized.
272 You can connect yourself to other running scripts here, as we do it
273 connect the extension modules (MackieControlXTs).
274 """
275 pass
276
278 """ Script -> Live.
279 When the internal MIDI controller has changed in a way that
280 you need to rebuild the MIDI mappings, request a rebuild
281 by calling this function This is processed as a request,
282 to be sure that its not too often called, because its
283 time-critical.
284 """
285 if not not self._in_build_midi_map:
286 raise AssertionError
287 self._suppress_requests_counter > 0 and self._rebuild_requests_during_suppression += 1
288 else:
289 self._c_instance.request_rebuild_midi_map()
290
292 """ Live -> Script
293 Build DeviceParameter Mappings, that are processed in Audio time, or
294 forward MIDI messages explicitly to our receive_midi_functions.
295 Which means that when you are not forwarding MIDI, nor mapping parameters,
296 you will never get any MIDI messages at all.
297 """
298 with self._in_build_midi_map():
299 self._forwarding_registry.clear()
300 self._forwarding_long_identifier_registry.clear()
301 for control in self.controls:
302 if isinstance(control, InputControlElement):
303 control.install_connections(self._translate_message, partial(self._install_mapping, midi_map_handle), partial(self._install_forwarding, midi_map_handle))
304
305 if self._pad_translations != None:
306 self._c_instance.set_pad_translation(self._pad_translations)
307
309 """ Script -> Live
310 Use this function to toggle the script's lock on devices
311 """
312 self._c_instance.toggle_lock()
313
315 """ Live -> Script
316 Send out MIDI to completely update the attached MIDI controller.
317 Will be called when requested by the user, after for example having reconnected
318 the MIDI cables...
319 """
320 self.update()
321
329
331 """ Live -> Script
332 Aka on_timer. Called every 100 ms and should be used to update display relevant
333 parts of the controller
334 """
335 with self.component_guard():
336 with self._is_sending_scheduled_messages():
337 self._task_group.update(Defaults.TIMER_DELAY)
338
340 """ Live -> Script
341 MIDI messages are only received through this function, when explicitly
342 forwarded in 'build_midi_map'.
343 """
344 with self.component_guard():
345 self._do_receive_midi(midi_bytes)
346
348 return len(midi_bytes) != 3
349
355
357 is_pitchbend = midi_bytes[0] & 240 == MIDI_PB_STATUS
358 forwarding_key = midi_bytes[:1 if is_pitchbend else 2]
359 value = midi_bytes[1] + (midi_bytes[2] << 7) if is_pitchbend else midi_bytes[2]
360 if forwarding_key in self._forwarding_registry:
361 recipient = self._forwarding_registry[forwarding_key]
362 if recipient != None:
363 recipient.receive_value(value)
364 else:
365 self.log_message('Got unknown message: ' + str(midi_bytes))
366
368 result = find_if(lambda (id, _): midi_bytes[:len(id)] == id, self._forwarding_long_identifier_registry.iteritems())
369 if result != None:
370 id, control = result
371 control.receive_value(midi_bytes[len(id):-1])
372 else:
373 self.log_message('Got unknown sysex message: ', midi_bytes)
374
376 raise self._device_component == None or AssertionError
377 raise device_component != None or AssertionError
378 raise isinstance(device_component, DeviceComponent) or AssertionError
379 self._device_component = device_component
380 self._device_component.set_lock_callback(self._toggle_lock)
381
382 @contextmanager
393
395 if not not self._in_build_midi_map:
396 raise AssertionError
397 suppress_requests and self._suppress_requests_counter += 1
398 elif not self._suppress_requests_counter > 0:
399 raise AssertionError
400 self._suppress_requests_counter -= 1
401 self._suppress_requests_counter == 0 and self._rebuild_requests_during_suppression > 0 and self.request_rebuild_midi_map()
402 self._rebuild_requests_during_suppression = 0
403
405 raise self._pad_translations == None or AssertionError
406 raise len(pad_translations) <= 16 or AssertionError
407
408 def check_translation(translation):
409 raise len(translation) == 4 or AssertionError
410 raise in_range(translation[0], 0, 4) or AssertionError
411 raise in_range(translation[1], 0, 4) or AssertionError
412 raise in_range(translation[2], 0, 128) or AssertionError
413 raise in_range(translation[3], 0, 16) or AssertionError
414 return True
415
416 raise all(map(check_translation, pad_translations)) or AssertionError
417 self._pad_translations = pad_translations
418
420 bool_enable = bool(enable)
421 if self._enabled != bool_enable:
422 with self.component_guard():
423 self._enabled = bool_enable
424 for component in self._components:
425 component._set_enabled_recursive(bool_enable)
426
428 """ Schedule a callback to be called after a specified time """
429 if not delay_in_ticks > 0:
430 raise AssertionError
431 if not callable(callback):
432 raise AssertionError
433 self._is_sending_scheduled_messages or delay_in_ticks -= 1
434 message_reference = [None]
435
436 def message(delta):
437 if parameter:
438 callback(parameter)
439 else:
440 callback()
441 self._remaining_scheduled_messages.remove(message_reference)
442
443 message_reference[0] = message
444 self._remaining_scheduled_messages.append(message_reference)
445 delay_in_ticks and self._task_group.add(Task.sequence(Task.delay(delay_in_ticks), message))
446 else:
447 self._task_group.add(message)
448
450 current_scheduled_messages = tuple(self._remaining_scheduled_messages)
451 for message, in current_scheduled_messages:
452 message(None)
453
456
458 """ Sets the track that will send its feedback to the control surface """
459 raise track == None or isinstance(track, Live.Track.Track) or AssertionError
460 self._c_instance.set_controlled_track(track)
461
470
472 """ puts component into the list of controls for triggering updates """
473 raise component != None or AssertionError
474 raise component not in self._components or AssertionError, 'Component registered twice'
475 self._components.append(component)
476 component.canonical_parent = self
477
478 @contextmanager
480 """
481 Context manager that guards user code. This prevents
482 unnecesary updating and enables several optimisations. Should
483 be used to guard calls to components or control elements.
484 """
485 if not self._in_component_guard:
486 with self._in_component_guard():
487 with self.setting_listener_caller():
488 with self._control_surface_injector:
489 with self.suppressing_rebuild_requests():
490 with self.accumulating_midi_messages():
491 yield
492 else:
493 yield
494
495 @property
497 return bool(self._in_component_guard)
498
499 @contextmanager
501 try:
502 self._c_instance.set_listener_caller(self._call_guarded_listener)
503 yield
504 finally:
505 self._c_instance.set_listener_caller(None)
506
518
519 @contextmanager
521 with self._accumulate_midi_messages():
522 try:
523 yield
524 finally:
525 self._flush_midi_messages()
526
527 - def _send_midi(self, midi_event_bytes, optimized = True):
528 """
529 Script -> Live
530 Use this function to send MIDI events through Live to the
531 _real_ MIDI devices that this script is assigned to.
532
533 When optimized=True it is assumed that messages can be
534 dropped -- only the last message within an update for a
535 given (channel, key) has visible effects.
536 """
537 if self._accumulate_midi_messages:
538 sysex_status_byte = 240
539 entry = (self._midi_message_count, midi_event_bytes)
540 if optimized and midi_event_bytes[0] != sysex_status_byte:
541 self._midi_message_dict[midi_event_bytes[0], midi_event_bytes[1]] = entry
542 else:
543 self._midi_message_list.append(entry)
544 self._midi_message_count += 1
545 else:
546 self._do_send_midi(midi_event_bytes)
547 return True
548
550 raise self._accumulate_midi_messages or AssertionError
551 for _, message in sorted(chain(self._midi_message_list, self._midi_message_dict.itervalues()), key=first):
552 self._do_send_midi(message)
553
554 self._midi_message_dict.clear()
555 self._midi_message_list[:] = []
556 self._midi_message_count = 0
557
559 self._c_instance.send_midi(midi_event_bytes)
560 return True
561
562 - def _install_mapping(self, midi_map_handle, control, parameter, feedback_delay, feedback_map):
563 if not self._in_build_midi_map:
564 raise AssertionError
565 raise control != None and parameter != None or AssertionError
566 raise isinstance(parameter, Live.DeviceParameter.DeviceParameter) or AssertionError
567 raise isinstance(control, InputControlElement) or AssertionError
568 raise isinstance(feedback_delay, int) or AssertionError
569 if not isinstance(feedback_map, tuple):
570 raise AssertionError
571 success = False
572 feedback_rule = None
573 feedback_rule = control.message_type() is MIDI_NOTE_TYPE and Live.MidiMap.NoteFeedbackRule()
574 feedback_rule.note_no = control.message_identifier()
575 feedback_rule.vel_map = feedback_map
576 elif control.message_type() is MIDI_CC_TYPE:
577 feedback_rule = Live.MidiMap.CCFeedbackRule()
578 feedback_rule.cc_no = control.message_identifier()
579 feedback_rule.cc_value_map = feedback_map
580 elif control.message_type() is MIDI_PB_TYPE:
581 feedback_rule = Live.MidiMap.PitchBendFeedbackRule()
582 feedback_rule.value_pair_map = feedback_map
583 if not feedback_rule != None:
584 raise AssertionError
585 feedback_rule.channel = control.message_channel()
586 feedback_rule.delay_in_ms = feedback_delay
587 success = control.message_type() is MIDI_NOTE_TYPE and Live.MidiMap.map_midi_note_with_feedback_map(midi_map_handle, parameter, control.message_channel(), control.message_identifier(), feedback_rule)
588 elif control.message_type() is MIDI_CC_TYPE:
589 success = Live.MidiMap.map_midi_cc_with_feedback_map(midi_map_handle, parameter, control.message_channel(), control.message_identifier(), control.message_map_mode(), feedback_rule, not control.needs_takeover(), control.mapping_sensitivity)
590 elif control.message_type() is MIDI_PB_TYPE:
591 success = Live.MidiMap.map_midi_pitchbend_with_feedback_map(midi_map_handle, parameter, control.message_channel(), feedback_rule, not control.needs_takeover())
592 success and Live.MidiMap.send_feedback_for_parameter(midi_map_handle, parameter)
593 return success
594
596 if not self._in_build_midi_map:
597 raise AssertionError
598 raise control != None or AssertionError
599 if not isinstance(control, InputControlElement):
600 raise AssertionError
601 success = False
602 success = control.message_type() is MIDI_NOTE_TYPE and Live.MidiMap.forward_midi_note(self._c_instance.handle(), midi_map_handle, control.message_channel(), control.message_identifier())
603 elif control.message_type() is MIDI_CC_TYPE:
604 success = Live.MidiMap.forward_midi_cc(self._c_instance.handle(), midi_map_handle, control.message_channel(), control.message_identifier())
605 elif control.message_type() is MIDI_PB_TYPE:
606 success = Live.MidiMap.forward_midi_pitchbend(self._c_instance.handle(), midi_map_handle, control.message_channel())
607 else:
608 raise control.message_type() == MIDI_SYSEX_TYPE or AssertionError
609 success = True
610 forwarding_keys = success and control.identifier_bytes()
611 for key in forwarding_keys:
612 registry = self._forwarding_registry if control.message_type() != MIDI_SYSEX_TYPE else self._forwarding_long_identifier_registry
613 raise key not in registry.keys() or AssertionError, 'Registry key %s registered twice. Check Midi messages!' % str(key)
614 registry[key] = control
615
616 return success
617
618 - def _translate_message(self, type, from_identifier, from_channel, to_identifier, to_channel):
619 if not type in (MIDI_CC_TYPE, MIDI_NOTE_TYPE):
620 raise AssertionError
621 raise from_identifier in range(128) or AssertionError
622 raise from_channel in range(16) or AssertionError
623 raise to_identifier in range(128) or AssertionError
624 raise to_channel in range(16) or AssertionError
625 type == MIDI_CC_TYPE and self._c_instance.set_cc_translation(from_identifier, from_channel, to_identifier, to_channel)
626 elif type == MIDI_NOTE_TYPE:
627 self._c_instance.set_note_translation(from_identifier, from_channel, to_identifier, to_channel)
628 else:
629 raise False or AssertionError
630
639
645
649
656
660
662 raise self._device_component != None or AssertionError
663 self._c_instance.toggle_lock()
664
666 """
667 Make sure the displays of the control surface display current
668 data.
669 """
670 for display in self._displays:
671 display.update()
672 display._tasks.update(Defaults.TIMER_DELAY)
673
675 track = self.song().view.selected_track
676 device_to_select = track.view.selected_device
677 if device_to_select == None and len(track.devices) > 0:
678 device_to_select = track.devices[0]
679 if device_to_select != None:
680 self.song().view.select_device(device_to_select)
681 self._device_component.set_device(self.song().appointed_device)
682 else:
683 self._device_component.set_device(None)
684