├── .gitignore ├── Makefile ├── __init__.py ├── BankToggleComponent.py ├── README.md ├── TransportComponent.py ├── QuantizationComponent.py ├── MixerComponent.py ├── Colors.py ├── LooperComponent.py └── APC40_MkII.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.zip 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all : 2 | python -m compileall *.py 3 | dist : 4 | python -m compileall *.py && mkdir apc40mkii_azuki && cp *.pyc apc40mkii_azuki/ && zip -r release.zip apc40mkii_azuki && rm -r apc40mkii_azuki 5 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # uncompyle6 version 3.4.1 2 | # Python bytecode 2.7 (62211) 3 | # Decompiled from: Python 2.7.16 (v2.7.16:413a49145e, Mar 2 2019, 14:32:10) 4 | # [GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] 5 | # Embedded file name: /Users/versonator/Jenkins/live/output/mac_64_static/Release/python-bundle/MIDI Remote Scripts/APC40_MkII/__init__.py 6 | # Compiled at: 2019-04-09 19:23:44 7 | from __future__ import absolute_import, print_function, unicode_literals 8 | from _Framework.Capabilities import CONTROLLER_ID_KEY, PORTS_KEY, NOTES_CC, SCRIPT, SYNC, REMOTE, controller_id, inport, outport 9 | from .APC40_MkII import APC40_MkII 10 | 11 | def create_instance(c_instance): 12 | return APC40_MkII(c_instance) 13 | 14 | 15 | def get_capabilities(): 16 | return {CONTROLLER_ID_KEY: controller_id(vendor_id=2536, product_ids=[ 17 | 41], model_name='Akai APC40 MkII'), 18 | PORTS_KEY: [ 19 | inport(props=[NOTES_CC, SCRIPT, REMOTE]), 20 | outport(props=[SYNC, SCRIPT, REMOTE])]} 21 | -------------------------------------------------------------------------------- /BankToggleComponent.py: -------------------------------------------------------------------------------- 1 | # uncompyle6 version 3.4.1 2 | # Python bytecode 2.7 (62211) 3 | # Decompiled from: Python 2.7.16 (v2.7.16:413a49145e, Mar 2 2019, 14:32:10) 4 | # [GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] 5 | # Embedded file name: /Users/versonator/Jenkins/live/output/mac_64_static/Release/python-bundle/MIDI Remote Scripts/APC40_MkII/BankToggleComponent.py 6 | # Compiled at: 2019-04-09 19:23:44 7 | from __future__ import absolute_import, print_function, unicode_literals 8 | from _Framework.ComboElement import ToggleElement 9 | from _Framework.Control import ToggleButtonControl 10 | from _Framework.ControlSurfaceComponent import ControlSurfaceComponent 11 | 12 | class BankToggleComponent(ControlSurfaceComponent): 13 | bank_toggle_button = ToggleButtonControl() 14 | 15 | def __init__(self, *a, **k): 16 | super(BankToggleComponent, self).__init__(*a, **k) 17 | self._toggle_elements = [] 18 | 19 | @bank_toggle_button.toggled 20 | def bank_toggle_button(self, toggled, button): 21 | for e in self._toggle_elements: 22 | e.set_toggled(toggled) 23 | 24 | def create_toggle_element(self, *a, **k): 25 | element = ToggleElement(*a, **k) 26 | element.toggled = self.bank_toggle_button.is_toggled 27 | self._toggle_elements.append(element) 28 | return element -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apc40mk2 2 | 3 | CDJ-style looping control mapping for the APC40MKII ableton midi controller 4 | 5 | ## prerequisites 6 | 7 | * ableton live 10.1 (may work with earlier versions but i haven't checked) 8 | * an APC40MKII controller 9 | 10 | ## installing 11 | 12 | 1. download release.zip from https://github.com/diracdeltas/apc40mk2/releases/latest 13 | 2. unzip it 14 | 3. copy the unzipped `apc40mkii_azuki` folder to your Ableton MIDI remote 15 | scripts directory using the instructions at 16 | https://help.ableton.com/hc/en-us/articles/209072009-Installing-Third-Party-Control-Surfaces. 17 | for instance on Mac with Ableton 10, this would be `/Applications/Ableton 18 | Live 10 Suite.app/Contents/App-Resources/MIDI Remote Scripts`. 19 | 4. open Ableton Preferences with your apc40mk2 plugged in and select `apc40mkii 20 | azuki` as the control surface. 21 | 22 | ## usage 23 | 24 | the custom mappings are as follows. note that all clips must be warped for this 25 | to work. 26 | 27 | * `METRONOME` - sets the loop start point to the nearest bar of the current playing position. if already looping, this turns off looping. 28 | * `TAP TEMPO` - sets loop end point to the nearest bar of the current playing position and engages looping. 29 | * `NUDGE-` - moves entire loop left by a bar 30 | * `NUDGE+` - moves entire loop right by a bar 31 | * `SHIFT NUDGE-` - halves the loop length 32 | * `SHIFT NUDGE+` - doubles the loop length. note: this will cause the play position to jump unnecessarily sometimes (i think this is an ableton bug) 33 | 34 | ## demo/tutorial 35 | 36 | https://www.youtube.com/watch?v=YILKOWhN2ag 37 | 38 | ## credits 39 | 40 | thanks to will marshall for doing most of the work for LooperComponent in https://github.com/willrjmarshall/AbletonDJTemplateUnsupported. 41 | 42 | ## license 43 | 44 | http://www.wtfpl.net/ 45 | -------------------------------------------------------------------------------- /TransportComponent.py: -------------------------------------------------------------------------------- 1 | # uncompyle6 version 3.4.1 2 | # Python bytecode 2.7 (62211) 3 | # Decompiled from: Python 2.7.16 (v2.7.16:413a49145e, Mar 2 2019, 14:32:10) 4 | # [GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] 5 | # Embedded file name: /Users/versonator/Jenkins/live/output/mac_64_static/Release/python-bundle/MIDI Remote Scripts/APC40_MkII/TransportComponent.py 6 | # Compiled at: 2019-04-09 19:23:44 7 | from __future__ import absolute_import, print_function, unicode_literals 8 | import Live 9 | from _Framework.Control import ButtonControl 10 | from _Framework.SubjectSlot import subject_slot 11 | from _Framework.TransportComponent import TransportComponent as TransportComponentBase 12 | from _Framework.Util import clamp 13 | 14 | class TransportComponent(TransportComponentBase): 15 | shift_button = ButtonControl() 16 | 17 | def __init__(self, *a, **k): 18 | 19 | def play_toggle_model_transform(val): 20 | if self.shift_button.is_pressed: 21 | return False 22 | return val 23 | 24 | k['play_toggle_model_transform'] = play_toggle_model_transform 25 | super(TransportComponent, self).__init__(*a, **k) 26 | self._tempo_encoder_control = None 27 | return 28 | 29 | def set_tempo_encoder(self, control): 30 | assert not control or control.message_map_mode() in ( 31 | Live.MidiMap.MapMode.relative_smooth_two_compliment, 32 | Live.MidiMap.MapMode.relative_two_compliment) 33 | if control != self._tempo_encoder_control: 34 | self._tempo_encoder_control = control 35 | self._tempo_encoder_value.subject = control 36 | self.update() 37 | 38 | @subject_slot('value') 39 | def _tempo_encoder_value(self, value): 40 | if self.is_enabled(): 41 | step = 0.1 if self.shift_button.is_pressed else 1.0 42 | amount = value - 128 if value >= 64 else value 43 | self.song().tempo = clamp(self.song().tempo + amount * step, 20, 999) -------------------------------------------------------------------------------- /QuantizationComponent.py: -------------------------------------------------------------------------------- 1 | # uncompyle6 version 3.4.1 2 | # Python bytecode 2.7 (62211) 3 | # Decompiled from: Python 2.7.16 (v2.7.16:413a49145e, Mar 2 2019, 14:32:10) 4 | # [GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] 5 | # Embedded file name: /Users/versonator/Jenkins/live/output/mac_64_static/Release/python-bundle/MIDI Remote Scripts/APC40_MkII/QuantizationComponent.py 6 | # Compiled at: 2019-04-09 19:23:44 7 | from __future__ import absolute_import, print_function, unicode_literals 8 | import Live 9 | from _Framework.Control import RadioButtonControl, control_list 10 | from _Framework.ControlSurfaceComponent import ControlSurfaceComponent 11 | from _Framework.SubjectSlot import subject_slot 12 | AVAILABLE_QUANTIZATION = [ 13 | Live.Song.Quantization.q_no_q, 14 | Live.Song.Quantization.q_8_bars, 15 | Live.Song.Quantization.q_4_bars, 16 | Live.Song.Quantization.q_2_bars, 17 | Live.Song.Quantization.q_bar, 18 | Live.Song.Quantization.q_quarter, 19 | Live.Song.Quantization.q_eight, 20 | Live.Song.Quantization.q_sixtenth] 21 | 22 | class QuantizationComponent(ControlSurfaceComponent): 23 | quantization_buttons = control_list(RadioButtonControl) 24 | 25 | def __init__(self, *a, **k): 26 | super(QuantizationComponent, self).__init__(*a, **k) 27 | self.quantization_buttons.control_count = len(AVAILABLE_QUANTIZATION) + 1 28 | self._on_clip_trigger_quantization_changed.subject = self.song() 29 | self._on_clip_trigger_quantization_changed() 30 | 31 | @quantization_buttons.checked 32 | def quantization_buttons(self, button): 33 | if 0 <= button.index < len(AVAILABLE_QUANTIZATION): 34 | quantization = AVAILABLE_QUANTIZATION[button.index] 35 | if quantization != self.song().clip_trigger_quantization: 36 | self.song().clip_trigger_quantization = quantization 37 | 38 | @subject_slot('clip_trigger_quantization') 39 | def _on_clip_trigger_quantization_changed(self): 40 | self._get_button(self.song().clip_trigger_quantization).is_checked = True 41 | 42 | def _get_button(self, quantization): 43 | if quantization in AVAILABLE_QUANTIZATION: 44 | return self.quantization_buttons[AVAILABLE_QUANTIZATION.index(quantization)] 45 | return self.quantization_buttons[(-1)] -------------------------------------------------------------------------------- /MixerComponent.py: -------------------------------------------------------------------------------- 1 | # uncompyle6 version 3.4.1 2 | # Python bytecode 2.7 (62211) 3 | # Decompiled from: Python 2.7.16 (v2.7.16:413a49145e, Mar 2 2019, 14:32:10) 4 | # [GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] 5 | # Embedded file name: /Users/versonator/Jenkins/live/output/mac_64_static/Release/python-bundle/MIDI Remote Scripts/APC40_MkII/MixerComponent.py 6 | # Compiled at: 2019-04-09 19:23:44 7 | from __future__ import absolute_import, print_function, unicode_literals 8 | from itertools import ifilter, izip_longest 9 | from _Framework.Control import RadioButtonControl, control_list 10 | from _Framework.Dependency import depends 11 | from _Framework.Util import nop 12 | from _APC.MixerComponent import MixerComponent as MixerComponentBase, ChanStripComponent as ChannelStripComponentBase 13 | 14 | class ChannelStripComponent(ChannelStripComponentBase): 15 | 16 | def _on_cf_assign_changed(self): 17 | if self.is_enabled() and self._crossfade_toggle: 18 | state = self._track.mixer_device.crossfade_assign if self._track else 1 19 | value_to_send = None 20 | if state == 0: 21 | value_to_send = 'Mixer.Crossfade.A' 22 | elif state == 1: 23 | value_to_send = 'Mixer.Crossfade.Off' 24 | elif state == 2: 25 | value_to_send = 'Mixer.Crossfade.B' 26 | self._crossfade_toggle.set_light(value_to_send) 27 | return 28 | 29 | 30 | def _set_channel(controls, channel): 31 | for control in ifilter(None, controls or []): 32 | control.set_channel(channel) 33 | 34 | return 35 | 36 | 37 | class MixerComponent(MixerComponentBase): 38 | send_select_buttons = control_list(RadioButtonControl) 39 | 40 | @depends(show_message=nop) 41 | def __init__(self, num_tracks=0, show_message=nop, *a, **k): 42 | super(MixerComponent, self).__init__(num_tracks=num_tracks, *a, **k) 43 | self._show_message = show_message 44 | self.on_num_sends_changed() 45 | self._pan_controls = None 46 | self._send_controls = None 47 | self._user_controls = None 48 | return 49 | 50 | def _create_strip(self): 51 | return ChannelStripComponent() 52 | 53 | @send_select_buttons.checked 54 | def send_select_buttons(self, button): 55 | self.send_index = button.index 56 | 57 | def on_num_sends_changed(self): 58 | self.send_select_buttons.control_count = self.num_sends 59 | 60 | def on_send_index_changed(self): 61 | if self.send_index is None: 62 | self.send_select_buttons.control_count = 0 63 | elif self.send_index < self.send_select_buttons.control_count: 64 | self.send_select_buttons[self.send_index].is_checked = True 65 | if self.is_enabled() and self._send_controls: 66 | self._show_controlled_sends_message() 67 | return 68 | 69 | def _show_controlled_sends_message(self): 70 | if self._send_index is not None: 71 | send_name = chr(ord('A') + self._send_index) 72 | self._show_message('Controlling Send %s' % send_name) 73 | return 74 | 75 | def set_pan_controls(self, controls): 76 | super(MixerComponent, self).set_pan_controls(controls) 77 | self._pan_controls = controls 78 | self._update_pan_controls() 79 | if self.is_enabled() and controls: 80 | self._show_message('Controlling Pans') 81 | 82 | def set_send_controls(self, controls): 83 | super(MixerComponent, self).set_send_controls(controls) 84 | self._send_controls = controls 85 | self._update_send_controls() 86 | if self.is_enabled() and controls: 87 | self._show_controlled_sends_message() 88 | 89 | def set_user_controls(self, controls): 90 | self._user_controls = controls 91 | self._update_user_controls() 92 | if self.is_enabled() and controls: 93 | self._show_message('Controlling User Mappings') 94 | 95 | def set_crossfade_buttons(self, buttons): 96 | for strip, button in izip_longest(self._channel_strips, buttons or []): 97 | strip.set_crossfade_toggle(button) 98 | 99 | def _update_pan_controls(self): 100 | _set_channel(self._pan_controls, 0) 101 | 102 | def _update_send_controls(self): 103 | _set_channel(self._send_controls, 1) 104 | 105 | def _update_user_controls(self): 106 | _set_channel(self._user_controls, 2) 107 | 108 | def update(self): 109 | super(MixerComponent, self).update() 110 | if self.is_enabled(): 111 | self._update_pan_controls() 112 | self._update_send_controls() 113 | self._update_user_controls() -------------------------------------------------------------------------------- /Colors.py: -------------------------------------------------------------------------------- 1 | # uncompyle6 version 3.4.1 2 | # Python bytecode 2.7 (62211) 3 | # Decompiled from: Python 2.7.16 (v2.7.16:413a49145e, Mar 2 2019, 14:32:10) 4 | # [GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] 5 | # Embedded file name: /Users/versonator/Jenkins/live/output/mac_64_static/Release/python-bundle/MIDI Remote Scripts/APC40_MkII/Colors.py 6 | # Compiled at: 2019-04-09 19:23:44 7 | from __future__ import absolute_import, print_function, unicode_literals 8 | LIVE_COLORS_TO_MIDI_VALUES = {10927616: 74, 9 | 16149507: 84, 10 | 4047616: 76, 11 | 6441901: 69, 12 | 14402304: 99, 13 | 8754719: 19, 14 | 16725558: 5, 15 | 3947580: 71, 16 | 10056267: 15, 17 | 8237133: 18, 18 | 12026454: 11, 19 | 12565097: 73, 20 | 13381230: 58, 21 | 12243060: 111, 22 | 16249980: 13, 23 | 13013643: 4, 24 | 10208397: 88, 25 | 695438: 65, 26 | 13821080: 110, 27 | 3101346: 46, 28 | 16749734: 107, 29 | 8962746: 102, 30 | 5538020: 79, 31 | 13684944: 117, 32 | 15064289: 119, 33 | 14183652: 94, 34 | 11442405: 44, 35 | 13408551: 100, 36 | 1090798: 78, 37 | 11096369: 127, 38 | 16753961: 96, 39 | 1769263: 87, 40 | 5480241: 64, 41 | 1698303: 90, 42 | 16773172: 97, 43 | 7491393: 126, 44 | 8940772: 80, 45 | 14837594: 10, 46 | 8912743: 16, 47 | 10060650: 105, 48 | 13872497: 14, 49 | 16753524: 108, 50 | 8092539: 70, 51 | 2319236: 39, 52 | 1716118: 47, 53 | 12349846: 59, 54 | 11481907: 121, 55 | 15029152: 57, 56 | 2490280: 25, 57 | 11119017: 112, 58 | 10701741: 81, 59 | 15597486: 8, 60 | 49071: 77, 61 | 10851765: 93, 62 | 12558270: 48, 63 | 32192: 43, 64 | 8758722: 103, 65 | 10204100: 104, 66 | 11958214: 55, 67 | 8623052: 66, 68 | 16726484: 95, 69 | 12581632: 86, 70 | 13958625: 28, 71 | 12173795: 115, 72 | 13482980: 116, 73 | 16777215: 3, 74 | 6094824: 33, 75 | 13496824: 114, 76 | 9611263: 92, 77 | 9160191: 36} 78 | RGB_COLOR_TABLE = ( 79 | (0, 0), 80 | (1, 1973790), 81 | (2, 8355711), 82 | (3, 16777215), 83 | (4, 16731212), 84 | (5, 16711680), 85 | (6, 5832704), 86 | (7, 1638400), 87 | (8, 16760172), 88 | (9, 16733184), 89 | (10, 5840128), 90 | (11, 2562816), 91 | (12, 16777036), 92 | (13, 16776960), 93 | (14, 5855488), 94 | (15, 1644800), 95 | (16, 8978252), 96 | (17, 5570304), 97 | (18, 1923328), 98 | (19, 1321728), 99 | (20, 5046092), 100 | (21, 65280), 101 | (22, 22784), 102 | (23, 6400), 103 | (24, 5046110), 104 | (25, 65305), 105 | (26, 22797), 106 | (27, 6402), 107 | (28, 5046152), 108 | (29, 65365), 109 | (30, 22813), 110 | (31, 7954), 111 | (32, 5046199), 112 | (33, 65433), 113 | (34, 22837), 114 | (35, 6418), 115 | (36, 5030911), 116 | (37, 43519), 117 | (38, 16722), 118 | (39, 4121), 119 | (40, 5015807), 120 | (41, 22015), 121 | (42, 7513), 122 | (43, 2073), 123 | (44, 5000447), 124 | (45, 255), 125 | (46, 89), 126 | (47, 25), 127 | (48, 8867071), 128 | (49, 5505279), 129 | (50, 1638500), 130 | (51, 983088), 131 | (52, 16731391), 132 | (53, 16711935), 133 | (54, 5832793), 134 | (55, 1638425), 135 | (56, 16731271), 136 | (57, 16711764), 137 | (58, 5832733), 138 | (59, 2228243), 139 | (60, 16717056), 140 | (61, 10040576), 141 | (62, 7950592), 142 | (63, 4416512), 143 | (64, 211200), 144 | (65, 22325), 145 | (66, 21631), 146 | (67, 255), 147 | (68, 17743), 148 | (69, 2425036), 149 | (70, 8355711), 150 | (71, 2105376), 151 | (72, 16711680), 152 | (73, 12451629), 153 | (74, 11529478), 154 | (75, 6618889), 155 | (76, 1084160), 156 | (77, 65415), 157 | (78, 43519), 158 | (79, 11007), 159 | (80, 4129023), 160 | (81, 7995647), 161 | (82, 11672189), 162 | (83, 4202752), 163 | (84, 16730624), 164 | (85, 8970502), 165 | (86, 7536405), 166 | (87, 65280), 167 | (88, 3931942), 168 | (89, 5898097), 169 | (90, 3735500), 170 | (91, 5999359), 171 | (92, 3232198), 172 | (93, 8880105), 173 | (94, 13835775), 174 | (95, 16711773), 175 | (96, 16744192), 176 | (97, 12169216), 177 | (98, 9502464), 178 | (99, 8609031), 179 | (100, 3746560), 180 | (101, 1330192), 181 | (102, 872504), 182 | (103, 1381674), 183 | (104, 1450074), 184 | (105, 6896668), 185 | (106, 11010058), 186 | (107, 14569789), 187 | (108, 14182940), 188 | (109, 16769318), 189 | (110, 10412335), 190 | (111, 6796559), 191 | (112, 1973808), 192 | (113, 14483307), 193 | (114, 8454077), 194 | (115, 10131967), 195 | (116, 9332479), 196 | (117, 4210752), 197 | (118, 7697781), 198 | (119, 14745599), 199 | (120, 10485760), 200 | (121, 3473408), 201 | (122, 1757184), 202 | (123, 475648), 203 | (124, 12169216), 204 | (125, 4141312), 205 | (126, 11755264), 206 | (127, 4920578)) -------------------------------------------------------------------------------- /LooperComponent.py: -------------------------------------------------------------------------------- 1 | from _Framework.ButtonElement import ButtonElement #added 2 | from _Framework.EncoderElement import EncoderElement #added 3 | 4 | 5 | def quantize(num): 6 | return round(num / 4.0) * 4.0 7 | 8 | 9 | class LooperComponent(): 10 | 'Handles looping controls' 11 | __module__ = __name__ 12 | 13 | 14 | def __init__(self, parent): 15 | self._parent = parent 16 | self._loop_on_button = None 17 | self._loop_off_button = None 18 | self._loop_double_button = None 19 | self._loop_halve_button = None 20 | self._clip_length = 0 21 | self._shift_button = None 22 | self._current_clip = None 23 | self._shift_pressed = False 24 | 25 | def get_loop_length(self): 26 | if self._current_clip != None: 27 | clip = self._current_clip 28 | return clip.loop_end - clip.loop_start 29 | else: 30 | return 0 31 | 32 | def set_loop_on_button(self, button): 33 | assert ((button == None) or (isinstance(button, ButtonElement) and button.is_momentary())) 34 | if self._loop_on_button != button: 35 | if self._loop_on_button != None: 36 | self._loop_on_button.remove_value_listener(self.start_loop) 37 | self._loop_on_button = button 38 | if (self._loop_on_button != None): 39 | self._loop_on_button.add_value_listener(self.start_loop) 40 | 41 | def start_loop(self, value): 42 | # toggles loop, sets start point to the current playing position 43 | if value > 0: 44 | self.get_current_clip() 45 | if self._current_clip != None: 46 | current_clip = self._current_clip 47 | if current_clip.looping == 1: 48 | current_clip.looping = 0 49 | else: 50 | if self._clip_length == 0: 51 | self._clip_length = current_clip.length 52 | current_position = current_clip.playing_position 53 | current_clip.looping = 1 54 | # set end to the end of the song for now 55 | current_clip.loop_end = quantize(self._clip_length) 56 | # set start to the current position 57 | current_clip.loop_start = quantize(current_position) 58 | 59 | 60 | def set_loop_off_button(self, button): 61 | assert ((button == None) or (isinstance(button, ButtonElement) and button.is_momentary())) 62 | if self._loop_off_button != button: 63 | if self._loop_off_button != None: 64 | self._loop_off_button.remove_value_listener(self.stop_loop) 65 | self._loop_off_button = button 66 | if (self._loop_off_button != None): 67 | self._loop_off_button.add_value_listener(self.stop_loop) 68 | 69 | def stop_loop(self, value): 70 | # sets end point to the current playing position 71 | if value > 0: 72 | self.get_current_clip() 73 | if self._current_clip != None: 74 | current_clip = self._current_clip 75 | current_position = current_clip.playing_position 76 | current_clip.loop_end = quantize(current_position) 77 | 78 | def set_loop_double_button(self, button): 79 | assert ((button == None) or (isinstance(button, ButtonElement) and button.is_momentary())) 80 | if self._loop_double_button != button: 81 | if self._loop_double_button != None: 82 | self._loop_double_button.remove_value_listener(self.increase_loop) 83 | self._loop_double_button = button 84 | if (self._loop_double_button != None): 85 | self._loop_double_button.add_value_listener(self.increase_loop) 86 | 87 | # Doubles loop with shift 88 | # Moves loop one bar right without shift 89 | def increase_loop(self, value): 90 | if value > 0: 91 | self.get_current_clip() 92 | if self._current_clip != None: 93 | current_clip = self._current_clip 94 | loop_length = self.get_loop_length() 95 | if self._shift_pressed: 96 | current_clip.loop_end = current_clip.loop_start + loop_length * 2.0 97 | else: 98 | current_clip.loop_end = current_clip.loop_end + 4.0 99 | current_clip.loop_start = current_clip.loop_start + 4.0 100 | 101 | 102 | def set_loop_halve_button(self, button): 103 | assert ((button == None) or (isinstance(button, ButtonElement) and button.is_momentary())) 104 | if self._loop_halve_button != button: 105 | if self._loop_halve_button != None: 106 | self._loop_halve_button.remove_value_listener(self.decrease_loop) 107 | self._loop_halve_button = button 108 | if (self._loop_halve_button != None): 109 | self._loop_halve_button.add_value_listener(self.decrease_loop) 110 | 111 | # halves loop with shift 112 | # left loop one bar right without shift 113 | def decrease_loop(self, value): 114 | if value > 0: 115 | self.get_current_clip() 116 | if self._current_clip != None: 117 | current_clip = self._current_clip 118 | loop_length = self.get_loop_length() 119 | if self._shift_pressed: 120 | current_clip.loop_end = current_clip.loop_start + loop_length / 2.0 121 | else: 122 | if current_clip.loop_start >= 4.0: 123 | current_clip.loop_end = current_clip.loop_end - 4.0 124 | current_clip.loop_start = current_clip.loop_start - 4.0 125 | else: 126 | current_clip.loop_end = 0.0 + loop_length 127 | current_clip.loop_start = 0.0 128 | 129 | def get_current_clip(self): 130 | if (self._parent.song().view.highlighted_clip_slot != None): 131 | clip_slot = self._parent.song().view.highlighted_clip_slot 132 | if clip_slot.has_clip: 133 | self._current_clip = clip_slot.clip 134 | else: 135 | self._current_clip = None 136 | else: 137 | self._current_clip = None 138 | 139 | 140 | def set_shift_button(self, button): #added 141 | assert ((button == None) or (isinstance(button, ButtonElement) and button.is_momentary())) 142 | if (self._shift_button != button): 143 | if (self._shift_button != None): 144 | self._shift_button.remove_value_listener(self._shift_value) 145 | self._shift_button = button 146 | if (self._shift_button != None): 147 | self._shift_button.add_value_listener(self._shift_value) 148 | 149 | def _shift_value(self, value): #added 150 | assert (self._shift_button != None) 151 | assert (value in range(128)) 152 | self._shift_pressed = (value != 0) 153 | -------------------------------------------------------------------------------- /APC40_MkII.py: -------------------------------------------------------------------------------- 1 | # uncompyle6 version 3.4.1 2 | # Python bytecode 2.7 (62211) 3 | # Decompiled from: Python 2.7.16 (v2.7.16:413a49145e, Mar 2 2019, 14:32:10) 4 | # [GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] 5 | # Embedded file name: /Users/versonator/Jenkins/live/output/mac_64_static/Release/python-bundle/MIDI Remote Scripts/APC40_MkII/APC40_MkII.py 6 | # Compiled at: 2019-04-09 19:23:44 7 | from __future__ import absolute_import, print_function, unicode_literals 8 | from functools import partial 9 | from _Framework.ButtonMatrixElement import ButtonMatrixElement 10 | from _Framework.ComboElement import ComboElement, DoublePressElement, MultiElement 11 | from _Framework.ControlSurface import OptimizedControlSurface 12 | from _Framework.Layer import Layer 13 | from _Framework.ModesComponent import ModesComponent, ImmediateBehaviour, DelayMode, AddLayerMode 14 | from _Framework.Resource import PrioritizedResource 15 | from _Framework.SessionRecordingComponent import SessionRecordingComponent 16 | from _Framework.SessionZoomingComponent import SessionZoomingComponent 17 | from _Framework.ClipCreator import ClipCreator 18 | from _Framework.Util import recursive_map 19 | from _APC.APC import APC 20 | from _APC.DeviceComponent import DeviceComponent 21 | from _APC.DeviceBankButtonElement import DeviceBankButtonElement 22 | from _APC.DetailViewCntrlComponent import DetailViewCntrlComponent 23 | from _APC.SessionComponent import SessionComponent 24 | from _APC.ControlElementUtils import make_button, make_encoder, make_slider, make_ring_encoder, make_pedal_button 25 | from _APC.SkinDefault import make_rgb_skin, make_default_skin, make_stop_button_skin, make_crossfade_button_skin 26 | from .Colors import * 27 | from .BankToggleComponent import BankToggleComponent 28 | from .LooperComponent import LooperComponent 29 | from .MixerComponent import MixerComponent 30 | from .QuantizationComponent import QuantizationComponent 31 | from .TransportComponent import TransportComponent 32 | NUM_TRACKS = 8 33 | NUM_SCENES = 5 34 | 35 | class APC40_MkII(APC, OptimizedControlSurface): 36 | 37 | def __init__(self, *a, **k): 38 | super(APC40_MkII, self).__init__(*a, **k) 39 | self._color_skin = make_rgb_skin() 40 | self._default_skin = make_default_skin() 41 | self._stop_button_skin = make_stop_button_skin() 42 | self._crossfade_button_skin = make_crossfade_button_skin() 43 | with self.component_guard(): 44 | self._create_controls() 45 | self._create_bank_toggle() 46 | self._create_session() 47 | self._create_mixer() 48 | self._create_transport() 49 | self._create_device() 50 | self._create_view_control() 51 | self._create_quantization_selection() 52 | self._create_recording() 53 | self._session.set_mixer(self._mixer) 54 | self.set_highlighting_session_component(self._session) 55 | self.set_device_component(self._device) 56 | 57 | def _with_shift(self, button): 58 | return ComboElement(button, modifiers=[self._shift_button]) 59 | 60 | def _create_controls(self): 61 | make_on_off_button = partial(make_button, skin=self._default_skin) 62 | 63 | def make_color_button(*a, **k): 64 | button = make_button(skin=self._color_skin, *a, **k) 65 | button.is_rgb = True 66 | button.num_delayed_messages = 2 67 | return button 68 | 69 | def make_matrix_button(track, scene): 70 | return make_color_button(0, 32 + track - NUM_TRACKS * scene, name='%d_Clip_%d_Button' % (track, scene)) 71 | 72 | def make_stop_button(track): 73 | return make_button(track, 52, name='%d_Stop_Button' % track, skin=self._stop_button_skin) 74 | 75 | self._shift_button = make_button(0, 98, name='Shift_Button', resource_type=PrioritizedResource) 76 | self._bank_button = make_on_off_button(0, 103, name='Bank_Button') 77 | self._left_button = make_button(0, 97, name='Bank_Select_Left_Button') 78 | self._right_button = make_button(0, 96, name='Bank_Select_Right_Button') 79 | self._up_button = make_button(0, 94, name='Bank_Select_Up_Button') 80 | self._down_button = make_button(0, 95, name='Bank_Select_Down_Button') 81 | self._stop_buttons = ButtonMatrixElement(rows=[[ make_stop_button(track) for track in xrange(NUM_TRACKS) ]]) 82 | self._stop_all_button = make_button(0, 81, name='Stop_All_Clips_Button') 83 | self._scene_launch_buttons_raw = [ make_color_button(0, scene + 82, name='Scene_%d_Launch_Button' % scene) for scene in xrange(NUM_SCENES) 84 | ] 85 | self._scene_launch_buttons = ButtonMatrixElement(rows=[ 86 | self._scene_launch_buttons_raw]) 87 | self._matrix_rows_raw = [ [ make_matrix_button(track, scene) for track in xrange(NUM_TRACKS) ] for scene in xrange(NUM_SCENES) 88 | ] 89 | self._session_matrix = ButtonMatrixElement(rows=self._matrix_rows_raw) 90 | self._pan_button = make_on_off_button(0, 87, name='Pan_Button') 91 | self._sends_button = make_on_off_button(0, 88, name='Sends_Button', resource_type=PrioritizedResource) 92 | self._user_button = make_on_off_button(0, 89, name='User_Button') 93 | self._mixer_encoders = ButtonMatrixElement(rows=[ 94 | [ make_ring_encoder(48 + track, 56 + track, name='Track_Control_%d' % track) for track in xrange(NUM_TRACKS) 95 | ]]) 96 | self._volume_controls = ButtonMatrixElement(rows=[ 97 | [ make_slider(track, 7, name='%d_Volume_Control' % track) for track in xrange(NUM_TRACKS) 98 | ]]) 99 | self._master_volume_control = make_slider(0, 14, name='Master_Volume_Control') 100 | self._prehear_control = make_encoder(0, 47, name='Prehear_Volume_Control') 101 | self._crossfader_control = make_slider(0, 15, name='Crossfader') 102 | self._raw_select_buttons = [ make_on_off_button(channel, 51, name='%d_Select_Button' % channel) for channel in xrange(NUM_TRACKS) 103 | ] 104 | self._arm_buttons = ButtonMatrixElement(rows=[ 105 | [ make_on_off_button(channel, 48, name='%d_Arm_Button' % channel) for channel in xrange(NUM_TRACKS) 106 | ]]) 107 | self._solo_buttons = ButtonMatrixElement(rows=[ 108 | [ make_on_off_button(channel, 49, name='%d_Solo_Button' % channel) for channel in xrange(NUM_TRACKS) 109 | ]]) 110 | self._mute_buttons = ButtonMatrixElement(rows=[ 111 | [ make_on_off_button(channel, 50, name='%d_Mute_Button' % channel) for channel in xrange(NUM_TRACKS) 112 | ]]) 113 | self._crossfade_buttons = ButtonMatrixElement(rows=[ 114 | [ make_button(channel, 66, name='%d_Crossfade_Button' % channel, skin=self._crossfade_button_skin) for channel in xrange(NUM_TRACKS) 115 | ]]) 116 | self._select_buttons = ButtonMatrixElement(rows=[ 117 | self._raw_select_buttons]) 118 | self._master_select_button = make_on_off_button(channel=0, identifier=80, name='Master_Select_Button') 119 | self._send_select_buttons = ButtonMatrixElement(rows=[ 120 | [ ComboElement(button, modifiers=[self._sends_button]) for button in self._raw_select_buttons 121 | ]]) 122 | self._quantization_buttons = ButtonMatrixElement(rows=[ 123 | [ ComboElement(button, modifiers=[self._shift_button]) for button in self._raw_select_buttons 124 | ]]) 125 | #self._metronome_button = make_on_off_button(0, 90, name='Metronome_Button') 126 | self._play_button = make_on_off_button(0, 91, name='Play_Button') 127 | self._record_button = make_on_off_button(0, 93, name='Record_Button') 128 | self._session_record_button = make_on_off_button(0, 102, name='Session_Record_Button') 129 | #self._nudge_down_button = make_button(0, 100, name='Nudge_Down_Button') 130 | #self._nudge_up_button = make_button(0, 101, name='Nudge_Up_Button') 131 | #self._tap_tempo_button = make_button(0, 99, name='Tap_Tempo_Button') 132 | self._tempo_control = make_encoder(0, 13, name='Tempo_Control') 133 | self._device_controls = ButtonMatrixElement(rows=[ 134 | [ make_ring_encoder(16 + index, 24 + index, name='Device_Control_%d' % index) for index in xrange(8) 135 | ]]) 136 | self._device_control_buttons_raw = [ make_on_off_button(0, 58 + index) for index in xrange(8) 137 | ] 138 | self._device_bank_buttons = ButtonMatrixElement(rows=[ 139 | [ DeviceBankButtonElement(button, modifiers=[self._shift_button]) for button in self._device_control_buttons_raw 140 | ]]) 141 | self._device_prev_bank_button = self._device_control_buttons_raw[2] 142 | self._device_prev_bank_button.name = 'Device_Prev_Bank_Button' 143 | self._device_next_bank_button = self._device_control_buttons_raw[3] 144 | self._device_next_bank_button.name = 'Device_Next_Bank_Button' 145 | self._device_on_off_button = self._device_control_buttons_raw[4] 146 | self._device_on_off_button.name = 'Device_On_Off_Button' 147 | self._device_lock_button = self._device_control_buttons_raw[5] 148 | self._device_lock_button.name = 'Device_Lock_Button' 149 | self._prev_device_button = self._device_control_buttons_raw[0] 150 | self._prev_device_button.name = 'Prev_Device_Button' 151 | self._next_device_button = self._device_control_buttons_raw[1] 152 | self._next_device_button.name = 'Next_Device_Button' 153 | self._clip_device_button = self._device_control_buttons_raw[6] 154 | self._clip_device_button.name = 'Clip_Device_Button' 155 | self._detail_view_button = self._device_control_buttons_raw[7] 156 | self._detail_view_button.name = 'Detail_View_Button' 157 | self._foot_pedal_button = DoublePressElement(make_pedal_button(64, name='Foot_Pedal')) 158 | self._shifted_matrix = ButtonMatrixElement(rows=recursive_map(self._with_shift, self._matrix_rows_raw)) 159 | self._shifted_scene_buttons = ButtonMatrixElement(rows=[ 160 | [ self._with_shift(button) for button in self._scene_launch_buttons_raw 161 | ]]) 162 | self._loop_on_button = make_button(0, 90) 163 | self._loop_off_button = make_button(0, 99) 164 | self._loop_halve_button = make_button(0, 100) 165 | self._loop_double_button = make_button(0, 101) 166 | looper = LooperComponent(self) 167 | looper.set_shift_button(self._shift_button) # currently unused 168 | looper.set_loop_on_button(self._loop_on_button) 169 | looper.set_loop_off_button(self._loop_off_button) 170 | looper.set_loop_double_button(self._loop_double_button) 171 | looper.set_loop_halve_button(self._loop_halve_button) 172 | 173 | def _create_bank_toggle(self): 174 | self._bank_toggle = BankToggleComponent(is_enabled=False, layer=Layer(bank_toggle_button=self._bank_button)) 175 | 176 | def _create_session(self): 177 | 178 | def when_bank_on(button): 179 | return self._bank_toggle.create_toggle_element(on_control=button) 180 | 181 | def when_bank_off(button): 182 | return self._bank_toggle.create_toggle_element(off_control=button) 183 | 184 | self._session = SessionComponent(NUM_TRACKS, NUM_SCENES, auto_name=True, is_enabled=False, enable_skinning=True, layer=Layer(track_bank_left_button=when_bank_off(self._left_button), track_bank_right_button=when_bank_off(self._right_button), scene_bank_up_button=when_bank_off(self._up_button), scene_bank_down_button=when_bank_off(self._down_button), page_left_button=when_bank_on(self._left_button), page_right_button=when_bank_on(self._right_button), page_up_button=when_bank_on(self._up_button), page_down_button=when_bank_on(self._down_button), stop_track_clip_buttons=self._stop_buttons, stop_all_clips_button=self._stop_all_button, scene_launch_buttons=self._scene_launch_buttons, clip_launch_buttons=self._session_matrix)) 185 | clip_color_table = LIVE_COLORS_TO_MIDI_VALUES.copy() 186 | clip_color_table[16777215] = 119 187 | self._session.set_rgb_mode(clip_color_table, RGB_COLOR_TABLE) 188 | self._session_zoom = SessionZoomingComponent(self._session, name='Session_Overview', enable_skinning=True, is_enabled=False, layer=Layer(button_matrix=self._shifted_matrix, nav_left_button=self._with_shift(self._left_button), nav_right_button=self._with_shift(self._right_button), nav_up_button=self._with_shift(self._up_button), nav_down_button=self._with_shift(self._down_button), scene_bank_buttons=self._shifted_scene_buttons)) 189 | 190 | def _create_mixer(self): 191 | self._mixer = MixerComponent(NUM_TRACKS, auto_name=True, is_enabled=False, invert_mute_feedback=True, layer=Layer(volume_controls=self._volume_controls, arm_buttons=self._arm_buttons, solo_buttons=self._solo_buttons, mute_buttons=self._mute_buttons, shift_button=self._shift_button, track_select_buttons=self._select_buttons, prehear_volume_control=self._prehear_control, crossfader_control=self._crossfader_control, crossfade_buttons=self._crossfade_buttons)) 192 | self._mixer.master_strip().layer = Layer(volume_control=self._master_volume_control, select_button=self._master_select_button) 193 | self._encoder_mode = ModesComponent(name='Encoder_Mode', is_enabled=False) 194 | self._encoder_mode.default_behaviour = ImmediateBehaviour() 195 | self._encoder_mode.add_mode('pan', [ 196 | AddLayerMode(self._mixer, Layer(pan_controls=self._mixer_encoders))]) 197 | self._encoder_mode.add_mode('sends', [ 198 | AddLayerMode(self._mixer, Layer(send_controls=self._mixer_encoders)), 199 | DelayMode(AddLayerMode(self._mixer, Layer(send_select_buttons=self._send_select_buttons)))]) 200 | self._encoder_mode.add_mode('user', [ 201 | AddLayerMode(self._mixer, Layer(user_controls=self._mixer_encoders))]) 202 | self._encoder_mode.layer = Layer(pan_button=self._pan_button, sends_button=self._sends_button, user_button=self._user_button) 203 | self._encoder_mode.selected_mode = 'pan' 204 | 205 | def _create_transport(self): 206 | self._transport = TransportComponent(name='Transport', is_enabled=False, layer=Layer(shift_button=self._shift_button, play_button=self._play_button, stop_button=ComboElement(self._play_button, modifiers=[ 207 | self._shift_button]), record_button=self._record_button, tempo_encoder=self._tempo_control), play_toggle_model_transform=lambda v: v) 208 | 209 | def _create_device(self): 210 | self._device = DeviceComponent(name='Device', is_enabled=False, layer=Layer(parameter_controls=self._device_controls, bank_buttons=self._device_bank_buttons, bank_prev_button=self._device_prev_bank_button, bank_next_button=self._device_next_bank_button, on_off_button=self._device_on_off_button, lock_button=self._device_lock_button), device_selection_follows_track_selection=True) 211 | 212 | def _create_view_control(self): 213 | self._view_control = DetailViewCntrlComponent(name='View_Control', is_enabled=False, layer=Layer(device_nav_left_button=self._prev_device_button, device_nav_right_button=self._next_device_button, device_clip_toggle_button=self._clip_device_button, detail_toggle_button=self._detail_view_button)) 214 | self._view_control.device_clip_toggle_button.pressed_color = 'DefaultButton.On' 215 | 216 | def _create_quantization_selection(self): 217 | self._quantization_selection = QuantizationComponent(name='Quantization_Selection', is_enabled=False, layer=Layer(quantization_buttons=self._quantization_buttons)) 218 | 219 | def _create_recording(self): 220 | record_button = MultiElement(self._session_record_button, self._foot_pedal_button.single_press) 221 | self._session_recording = SessionRecordingComponent(ClipCreator(), self._view_control, name='Session_Recording', is_enabled=False, layer=Layer(new_button=self._foot_pedal_button.double_press, record_button=record_button, _uses_foot_pedal=self._foot_pedal_button)) 222 | 223 | def get_matrix_button(self, column, row): 224 | return self._matrix_rows_raw[row][column] 225 | 226 | def _product_model_id_byte(self): 227 | return 41 228 | --------------------------------------------------------------------------------