├── LICENSE ├── README.md ├── YourControllerName - Live 11 ├── MIDI_Map.py ├── SpecialChannelStripComponent.py ├── SpecialMixerComponent.py ├── SpecialSessionComponent.py ├── SpecialTransportComponent.py ├── SpecialViewControllerComponent.py ├── SpecialZoomingComponent.py ├── YourControllerName.py └── __init__.py └── YourControllerName ├── MIDI_Map.py ├── SpecialChannelStripComponent.py ├── SpecialMixerComponent.py ├── SpecialSessionComponent.py ├── SpecialTransportComponent.py ├── SpecialViewControllerComponent.py ├── SpecialZoomingComponent.py ├── YourControllerName.py └── __init__.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Laidlaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ableton Live Custom MIDI Remote Scripts 2 | 3 | Scripts for creating custom MIDI Remote Scripts for Ableton Live. 4 | Known to be working with Live 9 and 10. I've added a new directory for Live 11 which updates the code to Python 3. 5 | Untested in Live 12. 6 | 7 | --- 8 | ### Background 9 | This is an [in-depth tutorial](https://youtu.be/IgKwcCJsoz4) on how to use these scripts. 10 | When I made the video 5 years ago, I had next to no programming knowledge, and I created it with the intent 11 | that non-programmers would be able to make their own MIDI scripts to interface with Live. 30k views later... it probably 12 | deserves an update. The video has more details about what to modify to get things working, but I'll add more comments 13 | to the code as I figure out what each function does. 14 | 15 | - Most of the code is from these [Behringer FCB1020 scripts](http://remotescripts.blogspot.com) by Hanz Petrov, if I 16 | can find their git or another source to credit them appropriately then I will do so. Let me know here if the site is gone, I have archived it for 17 | posterity. 18 | - For the Push 2, user [jzgdev](https://github.com/jzgdev/Push2UserModeScript) has a working Push 2 User Mode. 19 | - [More information](https://structure-void.com/ableton-live-midi-remote-scripts/) on MIDI remote scripts, the Live API 20 | documentation, Push scripts, and [decompiled scripts](https://github.com/gluon) for more current versions of Live by 21 | Julien Bayle. 22 | 23 | --- 24 | ### Instructions 25 | 26 | - Connect your MIDI device to your computer 27 | - Open Live and navigate to `Preferences → Link MIDI → MIDI` 28 | - Under either the Input or Output press the dropdown menu, note the exact name of your controller (if spaces are present in name, use underscores (e.g. "Arturia_Beatstep" 29 | instead of "Arturia Beatstep"). 30 | - Close Live. 31 | - Find and replace all instances in the scripts of `YourControllerName` folder with your controller's name. 32 | - There are three in `__init__.py` 33 | - and nine in `YourControllerName.py` 34 | - Save everything 35 | - Rename the YourControllerName folder and the `YourControllerName.py` with the same name. 36 | - Open `MIDI_Map.py` and inspect what you want to control. Unless stated otherwise, `-1` is the idle/unused value for 37 | each parameter. 38 | - You can use a third-party tool (see: [Downloads](#downloads)), or your device's user manual to figure out what MIDI 39 | channel, note or CC values each control on your device sends. 40 | I'd do this with pen and paper or [draw.io](https://draw.io). 41 | - Once you know your layout, assign the values to the respective parameter in `MIDI_Map.py` and ensure you set 42 | `BUTTONCHANNEL` and `SLIDERCHANNEL` to the same MIDI channel as your buttons and pots/faders. 43 | - Important: you must use the value for decimal number when assigning MIDI notes/buttons, DO NOT 44 | use note values like these C3, D4, A1 etc., they won't work. There are tables to work it out from the note, but the 45 | software is much more efficient and accurate. 46 | - Save and close everything and make a backup of your edited version, Live 9 compiles your scripts on launch, so you 47 | won't be able to make edits after it has done that. You've been warned. Don't let that effort be for nothing. 48 | 49 | MIDI Remote Scripts folder: 50 | - For Windows users: `\ProgramData\Ableton\Live x.x\Resources\MIDI Remote Scripts\` 51 | - For macOS users: 52 | - Locate the Live application in Finder (typically `/Applications/`), 53 | - right click on it and select "Show Package Contents" in the context menu, 54 | - navigate to: `/Contents/App-Resources/MIDI Remote Scripts/` 55 | - If you have multiple versions of Live installed, you will have to copy your scripts to each version individually. 56 | - If you are replacing a pre-existing script with the same device name then make a copy of the original. 57 | Otherwise, you will have to reinstall Live to get the default scripts back. 58 | - Launch Live and head back to `Preferences → Link MIDI → MIDI` 59 | - Under the `Control Surface` section, choose an empty slot and assign your newly created script. 60 | - Set the Input and Output to your MIDI device. 61 | - Under `MIDI Ports` check that `Remote` and `Track' are set to `On` for both the Input and the Output, or to 62 | your preference if you know what you're doing. 63 | - Test it out. 64 | - There is going to be a lot of trial and error here but it's pretty straightforward once you get an input working. 65 | 66 | 67 | --- 68 | ### Troubleshooting: 69 | - Make sure `YourControllerName` is changed everywhere, and you saved the script before placing it in the 70 | `MIDI Remote Scripts` directory. 71 | - Set the `BUTTONCHANNEL` and the `SLIDERCHANNEL` correctly. It won't work until you do that. 72 | - If you have multiple versions of Live installed, you will have to copy your scripts to each version individually. 73 | - Assign one control message (CC/Note) per parameter. 74 | - Before you add to `MIDI Remote Scripts` folder just make sure you have the correct MIDI channels and renamed 75 | YourControllerName in `__init__.py` and `YourControllerName.py`. Don't forget to change the folder and the file names 76 | as well. 77 | 78 | 79 | --- 80 | ### Updates 81 | - Change the size of the track selection box (coloured box in Session view). 82 | - There are now two variables in MIDI_Map.py to modify the size of the box: 83 | - `TSB_X` is the horizontal count i.e. the number of tracks 84 | - `TSB_Y` is the vertical count which represents the number of scenes you want to control. 85 | - For this to work properly you must manually add or remove rows/columns for `SCENELAUNCH` and `CLIPNOTEMAP` 86 | sections in [MIDI_Map.py](https://github.com/laidlaw42/Ableton-Live-MIDI-Remote-Scripts/blob/YourControllerName/YourControllerName/MIDI_Map.py). 87 | 88 | 89 | --- 90 | ### Downloads 91 | - [MIDI Monitor](https://www.snoize.com/midimonitor/) for macOS (preferred). 92 | - [MIDI Tools](https://mountainutilities.eu/miditools) for Windows and macOS. 93 | - [BCRManager](http://mountainutilities.eu/bcmanager) a GUI software solution for editing the Behringer BCR2000. 94 | - [Atom](https://atom.io) the code editor I mention in the video. 95 | - [PyCharm](https://www.jetbrains.com/pycharm/) probably a better editor to use if you're going to get into Python. 96 | -------------------------------------------------------------------------------- /YourControllerName - Live 11/MIDI_Map.py: -------------------------------------------------------------------------------- 1 | # Avoid using tabs for indentation, use spaces. 2 | # Don't use floats, they might break things. 3 | 4 | 5 | # Combination Mode offsets 6 | # ------------------------ 7 | 8 | TRACK_OFFSET = -1 # offset from the left of linked session origin; set to -1 for auto-joining of multiple instances 9 | SCENE_OFFSET = 0 # offset from the top of linked session origin (no auto-join) 10 | 11 | # Buttons / Pads 12 | # ------------- 13 | # Valid Note/CC assignments are 0 to 127, or -1 for NONE 14 | # Duplicate assignments are permitted 15 | 16 | BUTTONCHANNEL = 0 # Channel assignment for all mapped buttons/pads; valid range is 0 to 15 ; 0=1, 1=2 etc. 17 | MESSAGETYPE = 0 # Message type for buttons/pads; set to 0 for MIDI Notes, 1 for CCs. 18 | # When using CCs for buttons/pads, set BUTTONCHANNEL and SLIDERCHANNEL to different values. 19 | 20 | 21 | # Track selection box (aka that coloured box for scene/track launching) 22 | TSB_X = 8 # Controls the horizontal value for the track selection box. Default value is 8 23 | TSB_Y = 8 # Controls the horizontal value for the track selection box. Default value is 8 24 | 25 | # General 26 | PLAY = -1 # Global play 27 | STOP = -1 # Global stop 28 | REC = -1 # Global record 29 | TAPTEMPO = -1 # Tap tempo 30 | NUDGEUP = -1 # Tempo Nudge Up 31 | NUDGEDOWN = -1 # Tempo Nudge Down 32 | UNDO = -1 # Undo 33 | REDO = -1 # Redo 34 | LOOP = -1 # Loop on/off 35 | PUNCHIN = -1 # Punch in 36 | PUNCHOUT = -1 # Punch out 37 | OVERDUB = -1 # Overdub on/off 38 | METRONOME = -1 # Metronome on/off 39 | RECQUANT = -1 # Record quantization on/off 40 | DETAILVIEW = -1 # Detail view switch 41 | CLIPTRACKVIEW = -1 # Clip/Track view switch 42 | 43 | # Device Control 44 | DEVICELOCK = -1 # Device Lock (lock "blue hand") 45 | DEVICEONOFF = -1 # Device on/off 46 | DEVICENAVLEFT = -1 # Device nav left 47 | DEVICENAVRIGHT = -1 # Device nav right 48 | DEVICEBANKNAVLEFT = -1 # Device bank nav left 49 | DEVICEBANKNAVRIGHT = -1 # Device bank nav right 50 | DEVICEBANK = (-1, # Bank 1 #All 8 banks must be assigned to positive values in order for bank selection to work 51 | -1, # Bank 2 52 | -1, # Bank 3 53 | -1, # Bank 4 54 | -1, # Bank 5 55 | -1, # Bank 6 56 | -1, # Bank 7 57 | -1, # Bank 8 58 | ) 59 | 60 | # Arrangement View Controls 61 | SEEKFWD = -1 # Seek forward 62 | SEEKRWD = -1 # Seek rewind 63 | 64 | # Session Navigation (aka "red box") 65 | SESSIONLEFT = -1 # Session left 66 | SESSIONRIGHT = -1 # Session right 67 | SESSIONUP = -1 # Session up 68 | SESSIONDOWN = -1 # Session down 69 | ZOOMUP = -1 # Session Zoom up 70 | ZOOMDOWN = -1 # Session Zoom down 71 | ZOOMLEFT = -1 # Session Zoom left 72 | ZOOMRIGHT = -1 # Session Zoom right 73 | 74 | # Track Navigation 75 | TRACKLEFT = -1 # Track left 76 | TRACKRIGHT = -1 # Track right 77 | 78 | # Scene Navigation 79 | SCENEUP = -1 # Scene down 80 | SCENEDN = -1 # Scene up 81 | 82 | # Scene Launch 83 | SELSCENELAUNCH = -1 # Selected scene launch 84 | SCENELAUNCH = (-1, # Scene 1 Launch 85 | -1, # Scene 2 86 | -1, # Scene 3 87 | -1, # Scene 4 88 | -1, # Scene 5 89 | -1, # Scene 6 90 | -1, # Scene 7 91 | -1, # Scene 8 92 | ) 93 | 94 | # Clip Launch / Stop 95 | SELCLIPLAUNCH = -1 # Selected clip launch 96 | STOPALLCLIPS = -1 # Stop all clips 97 | 98 | # 8x8 Matrix note assignments 99 | # Track no.: 1 2 3 4 5 6 7 8 100 | CLIPNOTEMAP = ((-1, -1, -1, -1, -1, -1, -1, -1), # Row 1 101 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 2 102 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 3 103 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 4 104 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 5 105 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 6 106 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 7 107 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 8 108 | ) 109 | 110 | # Track Control 111 | MASTERSEL = -1 # Master track select 112 | SELTRACKREC = -1 # Arm Selected Track 113 | SELTRACKSOLO = -1 # Solo Selected Track 114 | SELTRACKMUTE = -1 # Mute Selected Track 115 | 116 | TRACKSTOP = (-1, # Track 1 Clip Stop 117 | -1, # Track 2 118 | -1, # Track 3 119 | -1, # Track 4 120 | -1, # Track 5 121 | -1, # Track 6 122 | -1, # Track 7 123 | -1, # Track 8 124 | ) 125 | 126 | TRACKSEL = (-1, # Track 1 Select 127 | -1, # Track 2 128 | -1, # Track 3 129 | -1, # Track 4 130 | -1, # Track 5 131 | -1, # Track 6 132 | -1, # Track 7 133 | -1, # Track 8 134 | ) 135 | 136 | TRACKMUTE = (-1, # Track 1 On/Off 137 | -1, # Track 2 138 | -1, # Track 3 139 | -1, # Track 4 140 | -1, # Track 5 141 | -1, # Track 6 142 | -1, # Track 7 143 | -1, # Track 8 144 | ) 145 | 146 | TRACKSOLO = (-1, # Track 1 Solo 147 | -1, # Track 2 148 | -1, # Track 3 149 | -1, # Track 4 150 | -1, # Track 5 151 | -1, # Track 6 152 | -1, # Track 7 153 | -1, # Track 8 154 | ) 155 | 156 | TRACKREC = (-1, # Track 1 Record 157 | -1, # Track 2 158 | -1, # Track 3 159 | -1, # Track 4 160 | -1, # Track 5 161 | -1, # Track 6 162 | -1, # Track 7 163 | -1, # Track 8 164 | ) 165 | 166 | 167 | # Pad Translations for Drum Rack 168 | PADCHANNEL = 0 # MIDI channel for Drum Rack notes 169 | DRUM_PADS = (-1, -1, -1, -1, # MIDI note numbers for 4 x 4 Drum Rack 170 | -1, -1, -1, -1, # Mapping will be disabled if any notes are set to -1 171 | -1, -1, -1, -1, # Notes will be "swallowed" if already mapped elsewhere 172 | -1, -1, -1, -1, 173 | ) 174 | 175 | 176 | # Sliders / Knobs 177 | # --------------- 178 | # Valid CC assignments are 0 to 127, or -1 for NONE 179 | # Duplicate assignments will be ignored 180 | SLIDERCHANNEL = 0 # Channel assignment for all mapped CCs; valid range is 0 to 15 181 | TEMPO_TOP = 180.0 # Upper limit of tempo control in BPM (max is 999) 182 | TEMPO_BOTTOM = 100.0 # Lower limit of tempo control in BPM (min is 0) 183 | 184 | TEMPOCONTROL = -1 # Tempo control CC assignment; control range is set above 185 | MASTERVOLUME = -1 # Master track volume 186 | CUELEVEL = -1 # Cue level control 187 | CROSSFADER = -1 # Crossfader control 188 | 189 | TRACKVOL = (-1, # Track 1 Volume 190 | -1, # Track 2 191 | -1, # Track 3 192 | -1, # Track 4 193 | -1, # Track 5 194 | -1, # Track 6 195 | -1, # Track 7 196 | -1, # Track 8 197 | ) 198 | 199 | TRACKPAN = (-1, # Track 1 Pan 200 | -1, # Track 2 201 | -1, # Track 3 202 | -1, # Track 4 203 | -1, # Track 5 204 | -1, # Track 6 205 | -1, # Track 7 206 | -1, # Track 8 207 | ) 208 | 209 | TRACKSENDA = (-1, # Track 1 Send A 210 | -1, # Track 2 211 | -1, # Track 3 212 | -1, # Track 4 213 | -1, # Track 5 214 | -1, # Track 6 215 | -1, # Track 7 216 | -1, # Track 8 217 | ) 218 | 219 | TRACKSENDB = (-1, # Track 1 Send B 220 | -1, # Track 2 221 | -1, # Track 3 222 | -1, # Track 4 223 | -1, # Track 5 224 | -1, # Track 6 225 | -1, # Track 7 226 | -1, # Track 8 227 | ) 228 | 229 | TRACKSENDC = (-1, # Track 1 Send C 230 | -1, # Track 2 231 | -1, # Track 3 232 | -1, # Track 4 233 | -1, # Track 5 234 | -1, # Track 6 235 | -1, # Track 7 236 | -1, # Track 8 237 | ) 238 | 239 | PARAMCONTROL = (-1, # Param 1 #All 8 params must be assigned to positive values in order for param control to work 240 | -1, # Param 2 241 | -1, # Param 3 242 | -1, # Param 4 243 | -1, # Param 5 244 | -1, # Param 6 245 | -1, # Param 7 246 | -1, # Param 8 247 | ) 248 | 249 | 250 | -------------------------------------------------------------------------------- /YourControllerName - Live 11/SpecialChannelStripComponent.py: -------------------------------------------------------------------------------- 1 | # emacs-mode: -*- python-*- 2 | # -*- coding: utf-8 -*- 3 | 4 | from _Framework.ChannelStripComponent import ChannelStripComponent 5 | TRACK_FOLD_DELAY = 5 6 | 7 | 8 | class SpecialChannelStripComponent(ChannelStripComponent): 9 | """ Subclass of channel strip component using select button for (un)folding tracks """ 10 | __module__ = __name__ 11 | 12 | def __init__(self): 13 | ChannelStripComponent.__init__(self) 14 | self._toggle_fold_ticks_delay = -1 15 | self._register_timer_callback(self._on_timer) 16 | 17 | def disconnect(self): 18 | self._unregister_timer_callback(self._on_timer) 19 | ChannelStripComponent.disconnect(self) 20 | 21 | def _select_value(self, value): 22 | ChannelStripComponent._select_value(self, value) 23 | if (self.is_enabled() and (self._track != None)): 24 | if (self._track.is_foldable and (self._select_button.is_momentary() and (value != 0))): 25 | self._toggle_fold_ticks_delay = TRACK_FOLD_DELAY 26 | else: 27 | self._toggle_fold_ticks_delay = -1 28 | 29 | def _on_timer(self): 30 | if (self.is_enabled() and (self._track != None)): 31 | if (self._toggle_fold_ticks_delay > -1): 32 | assert self._track.is_foldable 33 | if (self._toggle_fold_ticks_delay == 0): 34 | self._track.fold_state = (not self._track.fold_state) 35 | self._toggle_fold_ticks_delay -= 1 36 | 37 | 38 | # local variables: 39 | # tab-width: 4 40 | -------------------------------------------------------------------------------- /YourControllerName - Live 11/SpecialMixerComponent.py: -------------------------------------------------------------------------------- 1 | from _Framework.MixerComponent import MixerComponent 2 | from .SpecialChannelStripComponent import SpecialChannelStripComponent 3 | 4 | 5 | class SpecialMixerComponent(MixerComponent): 6 | """ Special mixer class that uses return tracks alongside midi and audio tracks """ 7 | __module__ = __name__ 8 | 9 | def __init__(self, num_tracks): 10 | MixerComponent.__init__(self, num_tracks) 11 | 12 | def tracks_to_use(self): 13 | return tuple(self.song().visible_tracks) + tuple(self.song().return_tracks) 14 | 15 | def _create_strip(self): 16 | return SpecialChannelStripComponent() 17 | -------------------------------------------------------------------------------- /YourControllerName - Live 11/SpecialSessionComponent.py: -------------------------------------------------------------------------------- 1 | # emacs-mode: -*- python-*- 2 | # -*- coding: utf-8 -*- 3 | 4 | import Live 5 | from _Framework.SessionComponent import SessionComponent 6 | from _Framework.ButtonElement import ButtonElement 7 | class SpecialSessionComponent(SessionComponent): 8 | " Special SessionComponent for APC combination mode and button to fire selected clip slot " 9 | __module__ = __name__ 10 | 11 | def __init__(self, num_tracks, num_scenes): 12 | SessionComponent.__init__(self, num_tracks, num_scenes) 13 | self._slot_launch_button = None 14 | 15 | def disconnect(self): 16 | SessionComponent.disconnect(self) 17 | if (self._slot_launch_button != None): 18 | self._slot_launch_button.remove_value_listener(self._slot_launch_value) 19 | self._slot_launch_button = None 20 | 21 | def link_with_track_offset(self, track_offset, scene_offset): 22 | assert (track_offset >= 0) 23 | assert (scene_offset >= 0) 24 | if self._is_linked(): 25 | self._unlink() 26 | self.set_offsets(track_offset, scene_offset) 27 | self._link() 28 | 29 | def unlink(self): 30 | if self._is_linked(): 31 | self._unlink() 32 | 33 | def set_slot_launch_button(self, button): 34 | assert ((button == None) or isinstance(button, ButtonElement)) 35 | if (self._slot_launch_button != button): 36 | if (self._slot_launch_button != None): 37 | self._slot_launch_button.remove_value_listener(self._slot_launch_value) 38 | self._slot_launch_button = button 39 | if (self._slot_launch_button != None): 40 | self._slot_launch_button.add_value_listener(self._slot_launch_value) 41 | 42 | self.update() 43 | 44 | def _slot_launch_value(self, value): 45 | assert (value in range(128)) 46 | assert (self._slot_launch_button != None) 47 | if self.is_enabled(): 48 | if ((value != 0) or (not self._slot_launch_button.is_momentary())): 49 | if (self.song().view.highlighted_clip_slot != None): 50 | self.song().view.highlighted_clip_slot.fire() 51 | 52 | 53 | 54 | # local variables: 55 | # tab-width: 4 56 | -------------------------------------------------------------------------------- /YourControllerName - Live 11/SpecialTransportComponent.py: -------------------------------------------------------------------------------- 1 | # There are lot of things to explore that you can use to add extra functionality to your scipt. 2 | # You'd essentially enable it here by uncommenting then add it YourControllerName.py and MIDI_Map. 3 | # I experiment more and add myself in the future. 4 | 5 | import Live 6 | from _Framework.TransportComponent import TransportComponent 7 | from _Framework.ButtonElement import ButtonElement 8 | from _Framework.EncoderElement import EncoderElement #added 9 | from _Framework.SubjectSlot import subject_slot #added 10 | #TEMPO_TOP = 300.0 11 | #TEMPO_BOTTOM = 40.0 12 | from .MIDI_Map import TEMPO_TOP 13 | from .MIDI_Map import TEMPO_BOTTOM 14 | class SpecialTransportComponent(TransportComponent): 15 | __doc__ = ' TransportComponent that only uses certain buttons if a shift button is pressed ' 16 | def __init__(self): 17 | TransportComponent.__init__(self) 18 | #self._shift_button = None 19 | self._quant_toggle_button = None 20 | #self._shift_pressed = False 21 | self._last_quant_value = Live.Song.RecordingQuantization.rec_q_eight 22 | self.song().add_midi_recording_quantization_listener(self._on_quantisation_changed) 23 | self._on_quantisation_changed() 24 | self._undo_button = None #added from OpenLabs SpecialTransportComponent script 25 | self._redo_button = None #added from OpenLabs SpecialTransportComponent script 26 | #self._bts_button = None #added from OpenLabs SpecialTransportComponent script 27 | self._tempo_encoder_control = None #new addition 28 | return None 29 | 30 | def disconnect(self): 31 | TransportComponent.disconnect(self) 32 | #if self._shift_button != None: 33 | #self._shift_button.remove_value_listener(self._shift_value) 34 | #self._shift_button = None 35 | if self._quant_toggle_button != None: 36 | self._quant_toggle_button.remove_value_listener(self._quant_toggle_value) 37 | self._quant_toggle_button = None 38 | self.song().remove_midi_recording_quantization_listener(self._on_quantisation_changed) 39 | if (self._undo_button != None): #added from OpenLabs SpecialTransportComponent script 40 | self._undo_button.remove_value_listener(self._undo_value) 41 | self._undo_button = None 42 | if (self._redo_button != None): #added from OpenLabs SpecialTransportComponent script 43 | self._redo_button.remove_value_listener(self._redo_value) 44 | self._redo_button = None 45 | #if (self._bts_button != None): #added from OpenLabs SpecialTransportComponent script 46 | #self._bts_button.remove_value_listener(self._bts_value) 47 | #self._bts_button = None 48 | if (self._tempo_encoder_control != None): #new addition 49 | self._tempo_encoder_control.remove_value_listener(self._tempo_encoder_value) 50 | self._tempo_encoder_control = None 51 | return None 52 | 53 | #def set_shift_button(self, button): 54 | #if not(button == None or isinstance(button, ButtonElement) and button.is_momentary()): 55 | #isinstance(button, ButtonElement) 56 | #raise AssertionError 57 | #if self._shift_button != button: 58 | #if self._shift_button != None: 59 | #self._shift_button.remove_value_listener(self._shift_value) 60 | #self._shift_button = button 61 | #if self._shift_button != None: 62 | #self._shift_button.add_value_listener(self._shift_value) 63 | # 64 | #self.update() 65 | #return None 66 | 67 | def set_quant_toggle_button(self, button): 68 | if not(button == None or isinstance(button, ButtonElement) and button.is_momentary()): 69 | isinstance(button, ButtonElement) 70 | raise AssertionError 71 | if self._quant_toggle_button != button: 72 | if self._quant_toggle_button != None: 73 | self._quant_toggle_button.remove_value_listener(self._quant_toggle_value) 74 | self._quant_toggle_button = button 75 | if self._quant_toggle_button != None: 76 | self._quant_toggle_button.add_value_listener(self._quant_toggle_value) 77 | 78 | self.update() 79 | return None 80 | 81 | #def update(self): 82 | #self._on_metronome_changed() 83 | #self._on_overdub_changed() 84 | #self._on_quantisation_changed() 85 | #self._on_nudge_up_changed() #added 86 | #self._on_nudge_down_changed #added 87 | 88 | #def _shift_value(self, value): 89 | #if not self._shift_button != None: 90 | #raise AssertionError 91 | #if not value in range(128): 92 | #raise AssertionError 93 | #self._shift_pressed = value != 0 94 | #if self.is_enabled(): 95 | #self.is_enabled() 96 | #self.update() 97 | #else: 98 | #self.is_enabled() 99 | #return None 100 | 101 | #def _metronome_value(self, value): 102 | #if not self._shift_pressed: 103 | ###if self._shift_pressed: 104 | #TransportComponent._metronome_value(self, value) 105 | 106 | 107 | #def _overdub_value(self, value): 108 | #if not self._shift_pressed: 109 | #TransportComponent._overdub_value(self, value) 110 | 111 | 112 | #def _nudge_up_value(self, value): #added 113 | #if not self._shift_pressed: 114 | #TransportComponent._nudge_up_value(self, value) 115 | 116 | 117 | #def _nudge_down_value(self, value): #added 118 | #if not self._shift_pressed: 119 | #TransportComponent._nudge_down_value(self, value) 120 | 121 | 122 | #def _tap_tempo_value(self, value): # Added as Shift + Tap Tempo 123 | #if not self._shift_pressed: 124 | ##if self._shift_pressed: 125 | #TransportComponent._tap_tempo_value(self, value) 126 | 127 | 128 | def _quant_toggle_value(self, value): 129 | assert (self._quant_toggle_button != None) 130 | assert (value in range(128)) 131 | assert (self._last_quant_value != Live.Song.RecordingQuantization.rec_q_no_q) 132 | if self.is_enabled(): # and (not self._shift_pressed): 133 | if ((value != 0) or (not self._quant_toggle_button.is_momentary())): 134 | quant_value = self.song().midi_recording_quantization 135 | if (quant_value != Live.Song.RecordingQuantization.rec_q_no_q): 136 | self._last_quant_value = quant_value 137 | self.song().midi_recording_quantization = Live.Song.RecordingQuantization.rec_q_no_q 138 | else: 139 | self.song().midi_recording_quantization = self._last_quant_value 140 | 141 | 142 | #def _on_metronome_changed(self): 143 | #if not self._shift_pressed: 144 | ##if self._shift_pressed: 145 | #TransportComponent._on_metronome_changed(self) 146 | 147 | 148 | #def _on_overdub_changed(self): 149 | #if not self._shift_pressed: 150 | #TransportComponent._on_overdub_changed(self) 151 | 152 | 153 | #def _on_nudge_up_changed(self): #added 154 | #if not self._shift_pressed: 155 | #TransportComponent._on_nudge_up_changed(self) 156 | 157 | 158 | #def _on_nudge_down_changed(self): #added 159 | #if not self._shift_pressed: 160 | #TransportComponent._on_nudge_down_changed(self) 161 | 162 | 163 | def _on_quantisation_changed(self): 164 | if self.is_enabled(): 165 | quant_value = self.song().midi_recording_quantization 166 | quant_on = (quant_value != Live.Song.RecordingQuantization.rec_q_no_q) 167 | if quant_on: 168 | self._last_quant_value = quant_value 169 | if self._quant_toggle_button != None: #((not self._shift_pressed) and (self._quant_toggle_button != None)): 170 | if quant_on: 171 | self._quant_toggle_button.turn_on() 172 | else: 173 | self._quant_toggle_button.turn_off() 174 | 175 | """ from OpenLabs module SpecialTransportComponent """ 176 | 177 | def set_undo_button(self, undo_button): 178 | assert isinstance(undo_button, (ButtonElement, 179 | type(None))) 180 | if (undo_button != self._undo_button): 181 | if (self._undo_button != None): 182 | self._undo_button.remove_value_listener(self._undo_value) 183 | self._undo_button = undo_button 184 | if (self._undo_button != None): 185 | self._undo_button.add_value_listener(self._undo_value) 186 | self.update() 187 | 188 | 189 | 190 | def set_redo_button(self, redo_button): 191 | assert isinstance(redo_button, (ButtonElement, 192 | type(None))) 193 | if (redo_button != self._redo_button): 194 | if (self._redo_button != None): 195 | self._redo_button.remove_value_listener(self._redo_value) 196 | self._redo_button = redo_button 197 | if (self._redo_button != None): 198 | self._redo_button.add_value_listener(self._redo_value) 199 | self.update() 200 | 201 | 202 | #def set_bts_button(self, bts_button): #"back to start" button 203 | #assert isinstance(bts_button, (ButtonElement, 204 | #type(None))) 205 | #if (bts_button != self._bts_button): 206 | #if (self._bts_button != None): 207 | #self._bts_button.remove_value_listener(self._bts_value) 208 | #self._bts_button = bts_button 209 | #if (self._bts_button != None): 210 | #self._bts_button.add_value_listener(self._bts_value) 211 | #self.update() 212 | 213 | 214 | def _undo_value(self, value): 215 | #if self._shift_pressed: #added 216 | assert (self._undo_button != None) 217 | assert (value in range(128)) 218 | if self.is_enabled(): 219 | if ((value != 0) or (not self._undo_button.is_momentary())): 220 | if self.song().can_undo: 221 | self.song().undo() 222 | 223 | 224 | def _redo_value(self, value): 225 | #if self._shift_pressed: #added 226 | assert (self._redo_button != None) 227 | assert (value in range(128)) 228 | if self.is_enabled(): 229 | if ((value != 0) or (not self._redo_button.is_momentary())): 230 | if self.song().can_redo: 231 | self.song().redo() 232 | 233 | 234 | #def _bts_value(self, value): 235 | #assert (self._bts_button != None) 236 | #assert (value in range(128)) 237 | #if self.is_enabled(): 238 | #if ((value != 0) or (not self._bts_button.is_momentary())): 239 | #self.song().current_song_time = 0.0 240 | 241 | 242 | def _tempo_encoder_value(self, value): 243 | ##if not self._shift_pressed: 244 | #if self._shift_pressed: 245 | assert (self._tempo_encoder_control != None) 246 | assert (value in range(128)) 247 | backwards = (value >= 64) 248 | step = 0.1 #step = 1.0 #reduce this for finer control; 1.0 is 1 bpm 249 | if backwards: 250 | amount = (value - 128) 251 | else: 252 | amount = value 253 | tempo = max(20, min(999, (self.song().tempo + (amount * step)))) 254 | self.song().tempo = tempo 255 | 256 | 257 | def set_tempo_encoder(self, control): 258 | assert ((control == None) or (isinstance(control, EncoderElement) and (control.message_map_mode() is Live.MidiMap.MapMode.relative_two_compliment))) 259 | if (self._tempo_encoder_control != None): 260 | self._tempo_encoder_control.remove_value_listener(self._tempo_encoder_value) 261 | self._tempo_encoder_control = control 262 | if (self._tempo_encoder_control != None): 263 | self._tempo_encoder_control.add_value_listener(self._tempo_encoder_value) 264 | self.update() 265 | 266 | @subject_slot('value') 267 | def _tempo_value(self, value): #Override to pull tempo range from MIDI_Maps.py 268 | assert (self._tempo_control != None) 269 | assert (value in range(128)) 270 | if self.is_enabled(): 271 | fraction = ((TEMPO_TOP - TEMPO_BOTTOM) / 127.0) 272 | self.song().tempo = ((fraction * value) + TEMPO_BOTTOM) 273 | 274 | -------------------------------------------------------------------------------- /YourControllerName - Live 11/SpecialViewControllerComponent.py: -------------------------------------------------------------------------------- 1 | # There are lot of things to explore that you can use to add extra functionality to your scipt. 2 | # You'd essentially enable it here by uncommenting then add it YourControllerName.py and MIDI_Map. 3 | # I experiment more and add myself in the future. 4 | 5 | # Partial --== Decompile ==-- with fixes 6 | import Live 7 | from _Framework.ControlSurfaceComponent import ControlSurfaceComponent 8 | from _Framework.ButtonElement import ButtonElement 9 | SHOW_PLAYING_CLIP_DELAY = 5 10 | class DetailViewControllerComponent(ControlSurfaceComponent): 11 | __module__ = __name__ 12 | __doc__ = ' Component that can toggle the device chain- and clip view of the selected track ' 13 | 14 | def __init__(self): 15 | ControlSurfaceComponent.__init__(self) 16 | self._device_clip_toggle_button = None 17 | self._detail_toggle_button = None 18 | self._left_button = None 19 | self._right_button = None 20 | #self._shift_button = None 21 | #self._shift_pressed = False 22 | self._show_playing_clip_ticks_delay = -1 23 | self.application().view.add_is_view_visible_listener('Detail', self._detail_view_visibility_changed) 24 | self._register_timer_callback(self._on_timer) 25 | return None 26 | 27 | def disconnect(self): 28 | self._unregister_timer_callback(self._on_timer) 29 | self.application().view.remove_is_view_visible_listener('Detail', self._detail_view_visibility_changed) 30 | if self._device_clip_toggle_button != None: 31 | self._device_clip_toggle_button.remove_value_listener(self._device_clip_toggle_value) 32 | self._device_clip_toggle_button = None 33 | if self._detail_toggle_button != None: 34 | self._detail_toggle_button.remove_value_listener(self._detail_toggle_value) 35 | self._detail_toggle_button = None 36 | if self._left_button != None: 37 | self._left_button.remove_value_listener(self._nav_value) 38 | self._left_button = None 39 | if self._right_button != None: 40 | self._right_button.remove_value_listener(self._nav_value) 41 | self._right_button = None 42 | #if self._shift_button != None: 43 | #self._shift_button.remove_value_listener(self._shift_value) 44 | #self._shift_button = None 45 | return None 46 | 47 | def set_device_clip_toggle_button(self, button): 48 | if not(button == None or isinstance(button, ButtonElement)): 49 | isinstance(button, ButtonElement) 50 | raise AssertionError 51 | if self._device_clip_toggle_button != button: 52 | if self._device_clip_toggle_button != None: 53 | self._device_clip_toggle_button.remove_value_listener(self._device_clip_toggle_value) 54 | self._device_clip_toggle_button = button 55 | if self._device_clip_toggle_button != None: 56 | self._device_clip_toggle_button.add_value_listener(self._device_clip_toggle_value) 57 | 58 | self.update() 59 | return None 60 | 61 | def set_detail_toggle_button(self, button): 62 | if not(button == None or isinstance(button, ButtonElement)): 63 | isinstance(button, ButtonElement) 64 | raise AssertionError 65 | if self._detail_toggle_button != button: 66 | if self._detail_toggle_button != None: 67 | self._detail_toggle_button.remove_value_listener(self._detail_toggle_value) 68 | self._detail_toggle_button = button 69 | if self._detail_toggle_button != None: 70 | self._detail_toggle_button.add_value_listener(self._detail_toggle_value) 71 | 72 | self.update() 73 | return None 74 | 75 | def set_device_nav_buttons(self, left_button, right_button): 76 | if not(left_button == None or isinstance(left_button, ButtonElement)): 77 | isinstance(left_button, ButtonElement) 78 | raise AssertionError 79 | if not(right_button == None or isinstance(right_button, ButtonElement)): 80 | isinstance(right_button, ButtonElement) 81 | raise AssertionError 82 | identify_sender = True 83 | if self._left_button != None: 84 | self._left_button.remove_value_listener(self._nav_value) 85 | self._left_button = left_button 86 | if self._left_button != None: 87 | self._left_button.add_value_listener(self._nav_value, identify_sender) 88 | if self._right_button != None: 89 | self._right_button.remove_value_listener(self._nav_value) 90 | self._right_button = right_button 91 | if self._right_button != None: 92 | self._right_button.add_value_listener(self._nav_value, identify_sender) 93 | 94 | self.update() 95 | return None 96 | 97 | #def set_shift_button(self, button): 98 | #if not(button == None or isinstance(button, ButtonElement) and button.is_momentary()): 99 | #isinstance(button, ButtonElement) 100 | #raise AssertionError 101 | #if self._shift_button != button: 102 | #if self._shift_button != None: 103 | #self._shift_button.remove_value_listener(self._shift_value) 104 | #self._shift_button = button 105 | #if self._shift_button != None: 106 | #self._shift_button.add_value_listener(self._shift_value) 107 | # 108 | #self.update() 109 | #return None 110 | 111 | def on_enabled_changed(self): 112 | self.update() 113 | 114 | def update(self): 115 | pass 116 | #if self.is_enabled(): 117 | #self.is_enabled() 118 | #if not self._shift_pressed: 119 | #self._shift_pressed 120 | #if self._left_button != None: 121 | #self._left_button.turn_off() 122 | #if self._right_button != None: 123 | #self._right_button.turn_off() 124 | #if self._device_clip_toggle_button != None: 125 | #self._device_clip_toggle_button.turn_off() 126 | #self._detail_view_visibility_changed() 127 | #else: 128 | #self._shift_pressed 129 | #else: 130 | #self.is_enabled() 131 | return None 132 | 133 | def _detail_view_visibility_changed(self): 134 | if self.is_enabled() and self._detail_toggle_button != None: #not self._shift_pressed and self._detail_toggle_button != None: 135 | if self.application().view.is_view_visible('Detail'): 136 | self.application().view.is_view_visible('Detail') 137 | self._detail_toggle_button.turn_on() 138 | else: 139 | self.application().view.is_view_visible('Detail') 140 | self._detail_toggle_button.turn_off() 141 | else: 142 | self.is_enabled() 143 | return None 144 | 145 | def _device_clip_toggle_value(self, value): 146 | if not self._device_clip_toggle_button != None: 147 | raise AssertionError 148 | if not value in range(128): 149 | raise AssertionError 150 | if self.is_enabled(): #and not self._shift_pressed: 151 | button_is_momentary = self._device_clip_toggle_button.is_momentary() 152 | if not button_is_momentary or value != 0: 153 | not button_is_momentary 154 | if not self.application().view.is_view_visible('Detail'): 155 | self.application().view.is_view_visible('Detail') 156 | self.application().view.show_view('Detail') 157 | else: 158 | self.application().view.is_view_visible('Detail') 159 | if not self.application().view.is_view_visible('Detail/DeviceChain'): 160 | self.application().view.is_view_visible('Detail/DeviceChain') 161 | self.application().view.show_view('Detail/DeviceChain') 162 | else: 163 | self.application().view.is_view_visible('Detail/DeviceChain') 164 | self.application().view.show_view('Detail/Clip') 165 | if button_is_momentary and value != 0: 166 | self._show_playing_clip_ticks_delay = SHOW_PLAYING_CLIP_DELAY 167 | else: 168 | button_is_momentary 169 | self._show_playing_clip_ticks_delay = -1 170 | else: 171 | self.is_enabled() 172 | return None 173 | 174 | 175 | def _detail_toggle_value(self, value): 176 | assert (self._detail_toggle_button != None) 177 | assert (value in range(128)) 178 | if self.is_enabled(): # and (not self._shift_pressed): 179 | if ((not self._detail_toggle_button.is_momentary()) or (value != 0)): 180 | if (not self.application().view.is_view_visible('Detail')): 181 | self.application().view.show_view('Detail') 182 | else: 183 | self.application().view.hide_view('Detail') 184 | 185 | 186 | #def _shift_value(self, value): 187 | #if not self._shift_button != None: 188 | #raise AssertionError 189 | #if not value in range(128): 190 | #raise AssertionError 191 | #self._shift_pressed = value != 0 192 | #self.update() 193 | #return None 194 | 195 | def _nav_value(self, value, sender): 196 | assert ((sender != None) and (sender in (self._left_button, 197 | self._right_button))) 198 | if self.is_enabled(): # and (not self._shift_pressed): 199 | if ((not sender.is_momentary()) or (value != 0)): 200 | modifier_pressed = True 201 | if ((not self.application().view.is_view_visible('Detail')) or (not self.application().view.is_view_visible('Detail/DeviceChain'))): 202 | self.application().view.show_view('Detail') 203 | self.application().view.show_view('Detail/DeviceChain') 204 | else: 205 | direction = Live.Application.Application.View.NavDirection.left 206 | if (sender == self._right_button): 207 | direction = Live.Application.Application.View.NavDirection.right 208 | self.application().view.scroll_view(direction, 'Detail/DeviceChain', (not modifier_pressed)) 209 | 210 | def _on_timer(self): 211 | if self.is_enabled(): # and (not self._shift_pressed): 212 | if (self._show_playing_clip_ticks_delay > -1): 213 | if (self._show_playing_clip_ticks_delay == 0): 214 | song = self.song() 215 | playing_slot_index = song.view.selected_track.playing_slot_index 216 | if (playing_slot_index > -1): 217 | song.view.selected_scene = song.scenes[playing_slot_index] 218 | if song.view.highlighted_clip_slot.has_clip: 219 | self.application().view.show_view('Detail/Clip') 220 | self._show_playing_clip_ticks_delay -= 1 221 | -------------------------------------------------------------------------------- /YourControllerName - Live 11/SpecialZoomingComponent.py: -------------------------------------------------------------------------------- 1 | # emacs-mode: -*- python-*- 2 | # -*- coding: utf-8 -*- 3 | 4 | import Live 5 | from _Framework.SessionZoomingComponent import SessionZoomingComponent 6 | from _Framework.ButtonElement import ButtonElement 7 | class SpecialZoomingComponent(SessionZoomingComponent): 8 | ' Special ZoomingComponent that uses clip stop buttons for stop all when zoomed ' 9 | __module__ = __name__ 10 | 11 | def __init__(self, session): 12 | SessionZoomingComponent.__init__(self, session) 13 | 14 | 15 | def _scroll_up(self): 16 | #if self._is_zoomed_out: 17 | height = self._session.height() 18 | track_offset = self._session.track_offset() 19 | scene_offset = self._session.scene_offset() 20 | 21 | if scene_offset > 0: 22 | new_scene_offset = scene_offset 23 | if scene_offset % height > 0: 24 | new_scene_offset -= (scene_offset % height) 25 | else: 26 | new_scene_offset = max(0, scene_offset - height) 27 | self._session.set_offsets(track_offset, new_scene_offset) 28 | 29 | def _scroll_down(self): 30 | #if self._is_zoomed_out: 31 | height = self._session.height() 32 | track_offset = self._session.track_offset() 33 | scene_offset = self._session.scene_offset() 34 | new_scene_offset = scene_offset + height - (scene_offset % height) 35 | self._session.set_offsets(track_offset, new_scene_offset) 36 | 37 | def _scroll_left(self): 38 | #if self._is_zoomed_out: 39 | width = self._session.width() 40 | track_offset = self._session.track_offset() 41 | scene_offset = self._session.scene_offset() 42 | if track_offset > 0: 43 | new_track_offset = track_offset 44 | if track_offset % width > 0: 45 | new_track_offset -= (track_offset % width) 46 | else: 47 | new_track_offset = max(0, track_offset - width) 48 | self._session.set_offsets(new_track_offset, scene_offset) 49 | 50 | def _scroll_right(self): 51 | #if self._is_zoomed_out: 52 | width = self._session.width() 53 | track_offset = self._session.track_offset() 54 | scene_offset = self._session.scene_offset() 55 | new_track_offset = track_offset + width - (track_offset % width) 56 | self._session.set_offsets(new_track_offset, scene_offset) 57 | -------------------------------------------------------------------------------- /YourControllerName - Live 11/YourControllerName.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import Live 4 | from _Framework.ControlSurface import ControlSurface 5 | from _Framework.InputControlElement import * 6 | from _Framework.SliderElement import SliderElement 7 | from _Framework.ButtonElement import ButtonElement 8 | from _Framework.ButtonMatrixElement import ButtonMatrixElement 9 | from _Framework.ChannelStripComponent import ChannelStripComponent 10 | from _Framework.DeviceComponent import DeviceComponent 11 | from _Framework.ControlSurfaceComponent import ControlSurfaceComponent 12 | from _Framework.SessionZoomingComponent import SessionZoomingComponent 13 | from .SpecialMixerComponent import SpecialMixerComponent 14 | from .SpecialTransportComponent import SpecialTransportComponent 15 | from .SpecialSessionComponent import SpecialSessionComponent 16 | from .SpecialZoomingComponent import SpecialZoomingComponent 17 | from .SpecialViewControllerComponent import DetailViewControllerComponent 18 | from .MIDI_Map import * 19 | 20 | 21 | # MIDI_NOTE_TYPE = 0 22 | # MIDI_CC_TYPE = 1 23 | # MIDI_PB_TYPE = 2 24 | 25 | 26 | class YourControllerName(ControlSurface): # Make sure you update the name 27 | __doc__ = " Script for YourControllerName in APC emulation mode " # Make sure you update the name 28 | 29 | _active_instances = [] 30 | 31 | def _combine_active_instances(): 32 | track_offset = 0 33 | scene_offset = 0 34 | for instance in YourControllerName._active_instances: # Make sure you update the name 35 | instance._activate_combination_mode(track_offset, scene_offset) 36 | track_offset += instance._session.width() 37 | _combine_active_instances = staticmethod(_combine_active_instances) 38 | 39 | def __init__(self, c_instance): 40 | ControlSurface.__init__(self, c_instance) 41 | # self.set_suppress_rebuild_requests(True) 42 | with self.component_guard(): 43 | self._note_map = [] 44 | self._ctrl_map = [] 45 | self._load_MIDI_map() 46 | self._session = None 47 | self._session_zoom = None 48 | self._mixer = None 49 | self._setup_session_control() 50 | self._setup_mixer_control() 51 | self._session.set_mixer(self._mixer) 52 | self._setup_device_and_transport_control() 53 | self.set_highlighting_session_component(self._session) 54 | # self.set_suppress_rebuild_requests(False) 55 | self._pads = [] 56 | self._load_pad_translations() 57 | self._do_combine() 58 | 59 | def disconnect(self): 60 | self._note_map = None 61 | self._ctrl_map = None 62 | self._pads = None 63 | self._do_uncombine() 64 | self._shift_button = None 65 | self._session = None 66 | self._session_zoom = None 67 | self._mixer = None 68 | ControlSurface.disconnect(self) 69 | 70 | def _do_combine(self): 71 | if self not in YourControllerName._active_instances: # Make sure you update the name 72 | YourControllerName._active_instances.append(self) # Make sure you update the name 73 | YourControllerName._combine_active_instances() # Make sure you update the name 74 | 75 | def _do_uncombine(self): 76 | if (self in YourControllerName._active_instances) and YourControllerName._active_instances.remove(self): # Make sure you update the name 77 | self._session.unlink() 78 | YourControllerName._combine_active_instances() # Make sure you update the name 79 | 80 | def _activate_combination_mode(self, track_offset, scene_offset): 81 | if TRACK_OFFSET != -1: 82 | track_offset = TRACK_OFFSET 83 | if SCENE_OFFSET != -1: 84 | scene_offset = SCENE_OFFSET 85 | self._session.link_with_track_offset(track_offset, scene_offset) 86 | 87 | def _setup_session_control(self): 88 | is_momentary = True 89 | self._session = SpecialSessionComponent(TSB_X, TSB_Y) # Track selection box size (X,Y) (horizontal, vertical). 90 | self._session.name = 'Session_Control' 91 | self._session.set_track_bank_buttons(self._note_map[SESSIONRIGHT], self._note_map[SESSIONLEFT]) 92 | self._session.set_scene_bank_buttons(self._note_map[SESSIONDOWN], self._note_map[SESSIONUP]) 93 | self._session.set_select_buttons(self._note_map[SCENEDN], self._note_map[SCENEUP]) 94 | # range(tsb_x) is the horizontal count for the track selection box 95 | self._scene_launch_buttons = [self._note_map[SCENELAUNCH[index]] for index in range(TSB_X)] 96 | # range(tsb_y) Range value is the track selection 97 | self._track_stop_buttons = [self._note_map[TRACKSTOP[index]] for index in range(TSB_Y)] 98 | self._session.set_stop_all_clips_button(self._note_map[STOPALLCLIPS]) 99 | self._session.set_stop_track_clip_buttons(tuple(self._track_stop_buttons)) 100 | self._session.selected_scene().name = 'Selected_Scene' 101 | self._session.selected_scene().set_launch_button(self._note_map[SELSCENELAUNCH]) 102 | self._session.set_slot_launch_button(self._note_map[SELCLIPLAUNCH]) 103 | for scene_index in range(TSB_Y): # Change range() value to set the vertical count for track selection box 104 | scene = self._session.scene(scene_index) 105 | scene.name = 'Scene_' + str(scene_index) 106 | button_row = [] 107 | scene.set_launch_button(self._scene_launch_buttons[scene_index]) 108 | scene.set_triggered_value(2) 109 | for track_index in range(TSB_X): # Change range() value to set the horizontal count for track selection box 110 | button = self._note_map[CLIPNOTEMAP[scene_index][track_index]] 111 | button_row.append(button) 112 | clip_slot = scene.clip_slot(track_index) 113 | clip_slot.name = str(track_index) + '_Clip_Slot_' + str(scene_index) 114 | clip_slot.set_launch_button(button) 115 | self._session_zoom = SpecialZoomingComponent(self._session) 116 | self._session_zoom.name = 'Session_Overview' 117 | self._session_zoom.set_nav_buttons(self._note_map[ZOOMUP], self._note_map[ZOOMDOWN], self._note_map[ZOOMLEFT], self._note_map[ZOOMRIGHT]) 118 | 119 | def _setup_mixer_control(self): 120 | 121 | is_momentary = True 122 | self._mixer = SpecialMixerComponent(8) 123 | self._mixer.name = 'Mixer' 124 | self._mixer.master_strip().name = 'Master_Channel_Strip' 125 | self._mixer.master_strip().set_select_button(self._note_map[MASTERSEL]) 126 | self._mixer.selected_strip().name = 'Selected_Channel_Strip' 127 | self._mixer.set_select_buttons(self._note_map[TRACKRIGHT], self._note_map[TRACKLEFT]) 128 | self._mixer.set_crossfader_control(self._ctrl_map[CROSSFADER]) 129 | self._mixer.set_prehear_volume_control(self._ctrl_map[CUELEVEL]) 130 | self._mixer.master_strip().set_volume_control(self._ctrl_map[MASTERVOLUME]) 131 | self._mixer.selected_strip().set_arm_button(self._note_map[SELTRACKREC]) 132 | self._mixer.selected_strip().set_solo_button(self._note_map[SELTRACKSOLO]) 133 | self._mixer.selected_strip().set_mute_button(self._note_map[SELTRACKMUTE]) 134 | for track in range(8): 135 | # My guess is that altering the range here will allow you to alter the range of mixer tracks 136 | # So if you had a 16 fader mixer, this would come in handy. 137 | strip = self._mixer.channel_strip(track) 138 | strip.name = 'Channel_Strip_' + str(track) 139 | strip.set_arm_button(self._note_map[TRACKREC[track]]) 140 | strip.set_solo_button(self._note_map[TRACKSOLO[track]]) 141 | strip.set_mute_button(self._note_map[TRACKMUTE[track]]) 142 | strip.set_select_button(self._note_map[TRACKSEL[track]]) 143 | strip.set_volume_control(self._ctrl_map[TRACKVOL[track]]) 144 | strip.set_pan_control(self._ctrl_map[TRACKPAN[track]]) 145 | strip.set_send_controls((self._ctrl_map[TRACKSENDA[track]], self._ctrl_map[TRACKSENDB[track]], self._ctrl_map[TRACKSENDC[track]])) 146 | strip.set_invert_mute_feedback(True) 147 | 148 | def _setup_device_and_transport_control(self): 149 | is_momentary = True 150 | self._device = DeviceComponent() 151 | self._device.name = 'Device_Component' 152 | device_bank_buttons = [] 153 | device_param_controls = [] 154 | for index in range(8): 155 | device_param_controls.append(self._ctrl_map[PARAMCONTROL[index]]) 156 | device_bank_buttons.append(self._note_map[DEVICEBANK[index]]) 157 | if None not in device_bank_buttons: 158 | self._device.set_bank_buttons(tuple(device_bank_buttons)) 159 | if None not in device_param_controls: 160 | self._device.set_parameter_controls(tuple(device_param_controls)) 161 | self._device.set_on_off_button(self._note_map[DEVICEONOFF]) 162 | self._device.set_bank_nav_buttons(self._note_map[DEVICEBANKNAVLEFT], self._note_map[DEVICEBANKNAVRIGHT]) 163 | self._device.set_lock_button(self._note_map[DEVICELOCK]) 164 | self.set_device_component(self._device) 165 | 166 | detail_view_toggler = DetailViewControllerComponent() 167 | detail_view_toggler.name = 'Detail_View_Control' 168 | detail_view_toggler.set_device_clip_toggle_button(self._note_map[CLIPTRACKVIEW]) 169 | detail_view_toggler.set_detail_toggle_button(self._note_map[DETAILVIEW]) 170 | detail_view_toggler.set_device_nav_buttons(self._note_map[DEVICENAVLEFT], self._note_map[DEVICENAVRIGHT]) 171 | 172 | transport = SpecialTransportComponent() 173 | transport.name = 'Transport' 174 | transport.set_play_button(self._note_map[PLAY]) 175 | transport.set_stop_button(self._note_map[STOP]) 176 | transport.set_record_button(self._note_map[REC]) 177 | transport.set_nudge_buttons(self._note_map[NUDGEUP], self._note_map[NUDGEDOWN]) 178 | transport.set_undo_button(self._note_map[UNDO]) 179 | transport.set_redo_button(self._note_map[REDO]) 180 | transport.set_tap_tempo_button(self._note_map[TAPTEMPO]) 181 | transport.set_quant_toggle_button(self._note_map[RECQUANT]) 182 | transport.set_overdub_button(self._note_map[OVERDUB]) 183 | transport.set_metronome_button(self._note_map[METRONOME]) 184 | transport.set_tempo_control(self._ctrl_map[TEMPOCONTROL]) 185 | transport.set_loop_button(self._note_map[LOOP]) 186 | transport.set_seek_buttons(self._note_map[SEEKFWD], self._note_map[SEEKRWD]) 187 | transport.set_punch_buttons(self._note_map[PUNCHIN], self._note_map[PUNCHOUT]) 188 | # transport.set_song_position_control(self._ctrl_map[SONGPOSITION]) #still not implemented as of Live 8.1.6 189 | 190 | def _on_selected_track_changed(self): 191 | ControlSurface._on_selected_track_changed(self) 192 | track = self.song().view.selected_track 193 | device_to_select = track.view.selected_device 194 | if device_to_select is None and len(track.devices) > 0: 195 | device_to_select = track.devices[0] 196 | if device_to_select is not None: 197 | self.song().view.select_device(device_to_select) 198 | self._device_component.set_device(device_to_select) 199 | 200 | def _load_pad_translations(self): 201 | if -1 not in DRUM_PADS: 202 | pad = [] 203 | for row in range(4): 204 | for col in range(4): 205 | pad = (col, row, DRUM_PADS[row*4 + col], PADCHANNEL,) 206 | self._pads.append(pad) 207 | self.set_pad_translations(tuple(self._pads)) 208 | 209 | def _load_MIDI_map(self): 210 | is_momentary = True 211 | for note in range(128): 212 | button = ButtonElement(is_momentary, MESSAGETYPE, BUTTONCHANNEL, note) 213 | button.name = 'Note_' + str(note) 214 | self._note_map.append(button) 215 | self._note_map.append(None) # add None to the end of the list, selectable with [-1] 216 | if MESSAGETYPE == MIDI_CC_TYPE and BUTTONCHANNEL == SLIDERCHANNEL: 217 | for ctrl in range(128): 218 | self._ctrl_map.append(None) 219 | else: 220 | for ctrl in range(128): 221 | control = SliderElement(MIDI_CC_TYPE, SLIDERCHANNEL, ctrl) 222 | control.name = 'Ctrl_' + str(ctrl) 223 | self._ctrl_map.append(control) 224 | self._ctrl_map.append(None) 225 | -------------------------------------------------------------------------------- /YourControllerName - Live 11/__init__.py: -------------------------------------------------------------------------------- 1 | import Live 2 | from .YourControllerName import YourControllerName 3 | 4 | 5 | def create_instance(c_instance): 6 | """ Creates and returns the APC20 script """ 7 | return YourControllerName(c_instance) 8 | 9 | # local variables: 10 | # tab-width: 4 11 | -------------------------------------------------------------------------------- /YourControllerName/MIDI_Map.py: -------------------------------------------------------------------------------- 1 | # Avoid using tabs for indentation, use spaces. 2 | # Don't use floats, they might break things. 3 | 4 | 5 | # Combination Mode offsets 6 | # ------------------------ 7 | 8 | TRACK_OFFSET = -1 # offset from the left of linked session origin; set to -1 for auto-joining of multiple instances 9 | SCENE_OFFSET = 0 # offset from the top of linked session origin (no auto-join) 10 | 11 | # Buttons / Pads 12 | # ------------- 13 | # Valid Note/CC assignments are 0 to 127, or -1 for NONE 14 | # Duplicate assignments are permitted 15 | 16 | BUTTONCHANNEL = 0 # Channel assignment for all mapped buttons/pads; valid range is 0 to 15 ; 0=1, 1=2 etc. 17 | MESSAGETYPE = 0 # Message type for buttons/pads; set to 0 for MIDI Notes, 1 for CCs. 18 | # When using CCs for buttons/pads, set BUTTONCHANNEL and SLIDERCHANNEL to different values. 19 | 20 | 21 | # Track selection box (aka that coloured box for scene/track launching) 22 | TSB_X = 8 # Controls the horizontal value for the track selection box. Default value is 8 23 | TSB_Y = 8 # Controls the horizontal value for the track selection box. Default value is 8 24 | 25 | # General 26 | PLAY = -1 # Global play 27 | STOP = -1 # Global stop 28 | REC = -1 # Global record 29 | TAPTEMPO = -1 # Tap tempo 30 | NUDGEUP = -1 # Tempo Nudge Up 31 | NUDGEDOWN = -1 # Tempo Nudge Down 32 | UNDO = -1 # Undo 33 | REDO = -1 # Redo 34 | LOOP = -1 # Loop on/off 35 | PUNCHIN = -1 # Punch in 36 | PUNCHOUT = -1 # Punch out 37 | OVERDUB = -1 # Overdub on/off 38 | METRONOME = -1 # Metronome on/off 39 | RECQUANT = -1 # Record quantization on/off 40 | DETAILVIEW = -1 # Detail view switch 41 | CLIPTRACKVIEW = -1 # Clip/Track view switch 42 | 43 | # Device Control 44 | DEVICELOCK = -1 # Device Lock (lock "blue hand") 45 | DEVICEONOFF = -1 # Device on/off 46 | DEVICENAVLEFT = -1 # Device nav left 47 | DEVICENAVRIGHT = -1 # Device nav right 48 | DEVICEBANKNAVLEFT = -1 # Device bank nav left 49 | DEVICEBANKNAVRIGHT = -1 # Device bank nav right 50 | DEVICEBANK = (-1, # Bank 1 #All 8 banks must be assigned to positive values in order for bank selection to work 51 | -1, # Bank 2 52 | -1, # Bank 3 53 | -1, # Bank 4 54 | -1, # Bank 5 55 | -1, # Bank 6 56 | -1, # Bank 7 57 | -1, # Bank 8 58 | ) 59 | 60 | # Arrangement View Controls 61 | SEEKFWD = -1 # Seek forward 62 | SEEKRWD = -1 # Seek rewind 63 | 64 | # Session Navigation (aka "red box") 65 | SESSIONLEFT = -1 # Session left 66 | SESSIONRIGHT = -1 # Session right 67 | SESSIONUP = -1 # Session up 68 | SESSIONDOWN = -1 # Session down 69 | ZOOMUP = -1 # Session Zoom up 70 | ZOOMDOWN = -1 # Session Zoom down 71 | ZOOMLEFT = -1 # Session Zoom left 72 | ZOOMRIGHT = -1 # Session Zoom right 73 | 74 | # Track Navigation 75 | TRACKLEFT = -1 # Track left 76 | TRACKRIGHT = -1 # Track right 77 | 78 | # Scene Navigation 79 | SCENEUP = -1 # Scene down 80 | SCENEDN = -1 # Scene up 81 | 82 | # Scene Launch 83 | SELSCENELAUNCH = -1 # Selected scene launch 84 | SCENELAUNCH = (-1, # Scene 1 Launch 85 | -1, # Scene 2 86 | -1, # Scene 3 87 | -1, # Scene 4 88 | -1, # Scene 5 89 | -1, # Scene 6 90 | -1, # Scene 7 91 | -1, # Scene 8 92 | ) 93 | 94 | # Clip Launch / Stop 95 | SELCLIPLAUNCH = -1 # Selected clip launch 96 | STOPALLCLIPS = -1 # Stop all clips 97 | 98 | # 8x8 Matrix note assignments 99 | # Track no.: 1 2 3 4 5 6 7 8 100 | CLIPNOTEMAP = ((-1, -1, -1, -1, -1, -1, -1, -1), # Row 1 101 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 2 102 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 3 103 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 4 104 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 5 105 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 6 106 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 7 107 | (-1, -1, -1, -1, -1, -1, -1, -1), # Row 8 108 | ) 109 | 110 | # Track Control 111 | MASTERSEL = -1 # Master track select 112 | SELTRACKREC = -1 # Arm Selected Track 113 | SELTRACKSOLO = -1 # Solo Selected Track 114 | SELTRACKMUTE = -1 # Mute Selected Track 115 | 116 | TRACKSTOP = (-1, # Track 1 Clip Stop 117 | -1, # Track 2 118 | -1, # Track 3 119 | -1, # Track 4 120 | -1, # Track 5 121 | -1, # Track 6 122 | -1, # Track 7 123 | -1, # Track 8 124 | ) 125 | 126 | TRACKSEL = (-1, # Track 1 Select 127 | -1, # Track 2 128 | -1, # Track 3 129 | -1, # Track 4 130 | -1, # Track 5 131 | -1, # Track 6 132 | -1, # Track 7 133 | -1, # Track 8 134 | ) 135 | 136 | TRACKMUTE = (-1, # Track 1 On/Off 137 | -1, # Track 2 138 | -1, # Track 3 139 | -1, # Track 4 140 | -1, # Track 5 141 | -1, # Track 6 142 | -1, # Track 7 143 | -1, # Track 8 144 | ) 145 | 146 | TRACKSOLO = (-1, # Track 1 Solo 147 | -1, # Track 2 148 | -1, # Track 3 149 | -1, # Track 4 150 | -1, # Track 5 151 | -1, # Track 6 152 | -1, # Track 7 153 | -1, # Track 8 154 | ) 155 | 156 | TRACKREC = (-1, # Track 1 Record 157 | -1, # Track 2 158 | -1, # Track 3 159 | -1, # Track 4 160 | -1, # Track 5 161 | -1, # Track 6 162 | -1, # Track 7 163 | -1, # Track 8 164 | ) 165 | 166 | 167 | # Pad Translations for Drum Rack 168 | PADCHANNEL = 0 # MIDI channel for Drum Rack notes 169 | DRUM_PADS = (-1, -1, -1, -1, # MIDI note numbers for 4 x 4 Drum Rack 170 | -1, -1, -1, -1, # Mapping will be disabled if any notes are set to -1 171 | -1, -1, -1, -1, # Notes will be "swallowed" if already mapped elsewhere 172 | -1, -1, -1, -1, 173 | ) 174 | 175 | 176 | # Sliders / Knobs 177 | # --------------- 178 | # Valid CC assignments are 0 to 127, or -1 for NONE 179 | # Duplicate assignments will be ignored 180 | SLIDERCHANNEL = 0 # Channel assignment for all mapped CCs; valid range is 0 to 15 181 | TEMPO_TOP = 180.0 # Upper limit of tempo control in BPM (max is 999) 182 | TEMPO_BOTTOM = 100.0 # Lower limit of tempo control in BPM (min is 0) 183 | 184 | TEMPOCONTROL = -1 # Tempo control CC assignment; control range is set above 185 | MASTERVOLUME = -1 # Master track volume 186 | CUELEVEL = -1 # Cue level control 187 | CROSSFADER = -1 # Crossfader control 188 | 189 | TRACKVOL = (-1, # Track 1 Volume 190 | -1, # Track 2 191 | -1, # Track 3 192 | -1, # Track 4 193 | -1, # Track 5 194 | -1, # Track 6 195 | -1, # Track 7 196 | -1, # Track 8 197 | ) 198 | 199 | TRACKPAN = (-1, # Track 1 Pan 200 | -1, # Track 2 201 | -1, # Track 3 202 | -1, # Track 4 203 | -1, # Track 5 204 | -1, # Track 6 205 | -1, # Track 7 206 | -1, # Track 8 207 | ) 208 | 209 | TRACKSENDA = (-1, # Track 1 Send A 210 | -1, # Track 2 211 | -1, # Track 3 212 | -1, # Track 4 213 | -1, # Track 5 214 | -1, # Track 6 215 | -1, # Track 7 216 | -1, # Track 8 217 | ) 218 | 219 | TRACKSENDB = (-1, # Track 1 Send B 220 | -1, # Track 2 221 | -1, # Track 3 222 | -1, # Track 4 223 | -1, # Track 5 224 | -1, # Track 6 225 | -1, # Track 7 226 | -1, # Track 8 227 | ) 228 | 229 | TRACKSENDC = (-1, # Track 1 Send C 230 | -1, # Track 2 231 | -1, # Track 3 232 | -1, # Track 4 233 | -1, # Track 5 234 | -1, # Track 6 235 | -1, # Track 7 236 | -1, # Track 8 237 | ) 238 | 239 | PARAMCONTROL = (-1, # Param 1 #All 8 params must be assigned to positive values in order for param control to work 240 | -1, # Param 2 241 | -1, # Param 3 242 | -1, # Param 4 243 | -1, # Param 5 244 | -1, # Param 6 245 | -1, # Param 7 246 | -1, # Param 8 247 | ) 248 | 249 | 250 | -------------------------------------------------------------------------------- /YourControllerName/SpecialChannelStripComponent.py: -------------------------------------------------------------------------------- 1 | # emacs-mode: -*- python-*- 2 | # -*- coding: utf-8 -*- 3 | 4 | from _Framework.ChannelStripComponent import ChannelStripComponent 5 | TRACK_FOLD_DELAY = 5 6 | 7 | 8 | class SpecialChannelStripComponent(ChannelStripComponent): 9 | """ Subclass of channel strip component using select button for (un)folding tracks """ 10 | __module__ = __name__ 11 | 12 | def __init__(self): 13 | ChannelStripComponent.__init__(self) 14 | self._toggle_fold_ticks_delay = -1 15 | self._register_timer_callback(self._on_timer) 16 | 17 | def disconnect(self): 18 | self._unregister_timer_callback(self._on_timer) 19 | ChannelStripComponent.disconnect(self) 20 | 21 | def _select_value(self, value): 22 | ChannelStripComponent._select_value(self, value) 23 | if (self.is_enabled() and (self._track != None)): 24 | if (self._track.is_foldable and (self._select_button.is_momentary() and (value != 0))): 25 | self._toggle_fold_ticks_delay = TRACK_FOLD_DELAY 26 | else: 27 | self._toggle_fold_ticks_delay = -1 28 | 29 | def _on_timer(self): 30 | if (self.is_enabled() and (self._track != None)): 31 | if (self._toggle_fold_ticks_delay > -1): 32 | assert self._track.is_foldable 33 | if (self._toggle_fold_ticks_delay == 0): 34 | self._track.fold_state = (not self._track.fold_state) 35 | self._toggle_fold_ticks_delay -= 1 36 | 37 | 38 | # local variables: 39 | # tab-width: 4 40 | -------------------------------------------------------------------------------- /YourControllerName/SpecialMixerComponent.py: -------------------------------------------------------------------------------- 1 | from _Framework.MixerComponent import MixerComponent 2 | from SpecialChannelStripComponent import SpecialChannelStripComponent 3 | 4 | 5 | class SpecialMixerComponent(MixerComponent): 6 | """ Special mixer class that uses return tracks alongside midi and audio tracks """ 7 | __module__ = __name__ 8 | 9 | def __init__(self, num_tracks): 10 | MixerComponent.__init__(self, num_tracks) 11 | 12 | def tracks_to_use(self): 13 | return tuple(self.song().visible_tracks) + tuple(self.song().return_tracks) 14 | 15 | def _create_strip(self): 16 | return SpecialChannelStripComponent() 17 | 18 | -------------------------------------------------------------------------------- /YourControllerName/SpecialSessionComponent.py: -------------------------------------------------------------------------------- 1 | # emacs-mode: -*- python-*- 2 | # -*- coding: utf-8 -*- 3 | 4 | import Live 5 | from _Framework.SessionComponent import SessionComponent 6 | from _Framework.ButtonElement import ButtonElement 7 | class SpecialSessionComponent(SessionComponent): 8 | " Special SessionComponent for APC combination mode and button to fire selected clip slot " 9 | __module__ = __name__ 10 | 11 | def __init__(self, num_tracks, num_scenes): 12 | SessionComponent.__init__(self, num_tracks, num_scenes) 13 | self._slot_launch_button = None 14 | 15 | def disconnect(self): 16 | SessionComponent.disconnect(self) 17 | if (self._slot_launch_button != None): 18 | self._slot_launch_button.remove_value_listener(self._slot_launch_value) 19 | self._slot_launch_button = None 20 | 21 | def link_with_track_offset(self, track_offset, scene_offset): 22 | assert (track_offset >= 0) 23 | assert (scene_offset >= 0) 24 | if self._is_linked(): 25 | self._unlink() 26 | self.set_offsets(track_offset, scene_offset) 27 | self._link() 28 | 29 | def unlink(self): 30 | if self._is_linked(): 31 | self._unlink() 32 | 33 | def set_slot_launch_button(self, button): 34 | assert ((button == None) or isinstance(button, ButtonElement)) 35 | if (self._slot_launch_button != button): 36 | if (self._slot_launch_button != None): 37 | self._slot_launch_button.remove_value_listener(self._slot_launch_value) 38 | self._slot_launch_button = button 39 | if (self._slot_launch_button != None): 40 | self._slot_launch_button.add_value_listener(self._slot_launch_value) 41 | 42 | self.update() 43 | 44 | def _slot_launch_value(self, value): 45 | assert (value in range(128)) 46 | assert (self._slot_launch_button != None) 47 | if self.is_enabled(): 48 | if ((value != 0) or (not self._slot_launch_button.is_momentary())): 49 | if (self.song().view.highlighted_clip_slot != None): 50 | self.song().view.highlighted_clip_slot.fire() 51 | 52 | 53 | 54 | # local variables: 55 | # tab-width: 4 56 | -------------------------------------------------------------------------------- /YourControllerName/SpecialTransportComponent.py: -------------------------------------------------------------------------------- 1 | # There are lot of things to explore that you can use to add extra functionality to your scipt. 2 | # You'd essentially enable it here by uncommenting then add it YourControllerName.py and MIDI_Map. 3 | # I experiment more and add myself in the future. 4 | 5 | import Live 6 | from _Framework.TransportComponent import TransportComponent 7 | from _Framework.ButtonElement import ButtonElement 8 | from _Framework.EncoderElement import EncoderElement #added 9 | from _Framework.SubjectSlot import subject_slot #added 10 | #TEMPO_TOP = 300.0 11 | #TEMPO_BOTTOM = 40.0 12 | from MIDI_Map import TEMPO_TOP 13 | from MIDI_Map import TEMPO_BOTTOM 14 | class SpecialTransportComponent(TransportComponent): 15 | __doc__ = ' TransportComponent that only uses certain buttons if a shift button is pressed ' 16 | def __init__(self): 17 | TransportComponent.__init__(self) 18 | #self._shift_button = None 19 | self._quant_toggle_button = None 20 | #self._shift_pressed = False 21 | self._last_quant_value = Live.Song.RecordingQuantization.rec_q_eight 22 | self.song().add_midi_recording_quantization_listener(self._on_quantisation_changed) 23 | self._on_quantisation_changed() 24 | self._undo_button = None #added from OpenLabs SpecialTransportComponent script 25 | self._redo_button = None #added from OpenLabs SpecialTransportComponent script 26 | #self._bts_button = None #added from OpenLabs SpecialTransportComponent script 27 | self._tempo_encoder_control = None #new addition 28 | return None 29 | 30 | def disconnect(self): 31 | TransportComponent.disconnect(self) 32 | #if self._shift_button != None: 33 | #self._shift_button.remove_value_listener(self._shift_value) 34 | #self._shift_button = None 35 | if self._quant_toggle_button != None: 36 | self._quant_toggle_button.remove_value_listener(self._quant_toggle_value) 37 | self._quant_toggle_button = None 38 | self.song().remove_midi_recording_quantization_listener(self._on_quantisation_changed) 39 | if (self._undo_button != None): #added from OpenLabs SpecialTransportComponent script 40 | self._undo_button.remove_value_listener(self._undo_value) 41 | self._undo_button = None 42 | if (self._redo_button != None): #added from OpenLabs SpecialTransportComponent script 43 | self._redo_button.remove_value_listener(self._redo_value) 44 | self._redo_button = None 45 | #if (self._bts_button != None): #added from OpenLabs SpecialTransportComponent script 46 | #self._bts_button.remove_value_listener(self._bts_value) 47 | #self._bts_button = None 48 | if (self._tempo_encoder_control != None): #new addition 49 | self._tempo_encoder_control.remove_value_listener(self._tempo_encoder_value) 50 | self._tempo_encoder_control = None 51 | return None 52 | 53 | #def set_shift_button(self, button): 54 | #if not(button == None or isinstance(button, ButtonElement) and button.is_momentary()): 55 | #isinstance(button, ButtonElement) 56 | #raise AssertionError 57 | #if self._shift_button != button: 58 | #if self._shift_button != None: 59 | #self._shift_button.remove_value_listener(self._shift_value) 60 | #self._shift_button = button 61 | #if self._shift_button != None: 62 | #self._shift_button.add_value_listener(self._shift_value) 63 | # 64 | #self.update() 65 | #return None 66 | 67 | def set_quant_toggle_button(self, button): 68 | if not(button == None or isinstance(button, ButtonElement) and button.is_momentary()): 69 | isinstance(button, ButtonElement) 70 | raise AssertionError 71 | if self._quant_toggle_button != button: 72 | if self._quant_toggle_button != None: 73 | self._quant_toggle_button.remove_value_listener(self._quant_toggle_value) 74 | self._quant_toggle_button = button 75 | if self._quant_toggle_button != None: 76 | self._quant_toggle_button.add_value_listener(self._quant_toggle_value) 77 | 78 | self.update() 79 | return None 80 | 81 | #def update(self): 82 | #self._on_metronome_changed() 83 | #self._on_overdub_changed() 84 | #self._on_quantisation_changed() 85 | #self._on_nudge_up_changed() #added 86 | #self._on_nudge_down_changed #added 87 | 88 | #def _shift_value(self, value): 89 | #if not self._shift_button != None: 90 | #raise AssertionError 91 | #if not value in range(128): 92 | #raise AssertionError 93 | #self._shift_pressed = value != 0 94 | #if self.is_enabled(): 95 | #self.is_enabled() 96 | #self.update() 97 | #else: 98 | #self.is_enabled() 99 | #return None 100 | 101 | #def _metronome_value(self, value): 102 | #if not self._shift_pressed: 103 | ###if self._shift_pressed: 104 | #TransportComponent._metronome_value(self, value) 105 | 106 | 107 | #def _overdub_value(self, value): 108 | #if not self._shift_pressed: 109 | #TransportComponent._overdub_value(self, value) 110 | 111 | 112 | #def _nudge_up_value(self, value): #added 113 | #if not self._shift_pressed: 114 | #TransportComponent._nudge_up_value(self, value) 115 | 116 | 117 | #def _nudge_down_value(self, value): #added 118 | #if not self._shift_pressed: 119 | #TransportComponent._nudge_down_value(self, value) 120 | 121 | 122 | #def _tap_tempo_value(self, value): # Added as Shift + Tap Tempo 123 | #if not self._shift_pressed: 124 | ##if self._shift_pressed: 125 | #TransportComponent._tap_tempo_value(self, value) 126 | 127 | 128 | def _quant_toggle_value(self, value): 129 | assert (self._quant_toggle_button != None) 130 | assert (value in range(128)) 131 | assert (self._last_quant_value != Live.Song.RecordingQuantization.rec_q_no_q) 132 | if self.is_enabled(): # and (not self._shift_pressed): 133 | if ((value != 0) or (not self._quant_toggle_button.is_momentary())): 134 | quant_value = self.song().midi_recording_quantization 135 | if (quant_value != Live.Song.RecordingQuantization.rec_q_no_q): 136 | self._last_quant_value = quant_value 137 | self.song().midi_recording_quantization = Live.Song.RecordingQuantization.rec_q_no_q 138 | else: 139 | self.song().midi_recording_quantization = self._last_quant_value 140 | 141 | 142 | #def _on_metronome_changed(self): 143 | #if not self._shift_pressed: 144 | ##if self._shift_pressed: 145 | #TransportComponent._on_metronome_changed(self) 146 | 147 | 148 | #def _on_overdub_changed(self): 149 | #if not self._shift_pressed: 150 | #TransportComponent._on_overdub_changed(self) 151 | 152 | 153 | #def _on_nudge_up_changed(self): #added 154 | #if not self._shift_pressed: 155 | #TransportComponent._on_nudge_up_changed(self) 156 | 157 | 158 | #def _on_nudge_down_changed(self): #added 159 | #if not self._shift_pressed: 160 | #TransportComponent._on_nudge_down_changed(self) 161 | 162 | 163 | def _on_quantisation_changed(self): 164 | if self.is_enabled(): 165 | quant_value = self.song().midi_recording_quantization 166 | quant_on = (quant_value != Live.Song.RecordingQuantization.rec_q_no_q) 167 | if quant_on: 168 | self._last_quant_value = quant_value 169 | if self._quant_toggle_button != None: #((not self._shift_pressed) and (self._quant_toggle_button != None)): 170 | if quant_on: 171 | self._quant_toggle_button.turn_on() 172 | else: 173 | self._quant_toggle_button.turn_off() 174 | 175 | """ from OpenLabs module SpecialTransportComponent """ 176 | 177 | def set_undo_button(self, undo_button): 178 | assert isinstance(undo_button, (ButtonElement, 179 | type(None))) 180 | if (undo_button != self._undo_button): 181 | if (self._undo_button != None): 182 | self._undo_button.remove_value_listener(self._undo_value) 183 | self._undo_button = undo_button 184 | if (self._undo_button != None): 185 | self._undo_button.add_value_listener(self._undo_value) 186 | self.update() 187 | 188 | 189 | 190 | def set_redo_button(self, redo_button): 191 | assert isinstance(redo_button, (ButtonElement, 192 | type(None))) 193 | if (redo_button != self._redo_button): 194 | if (self._redo_button != None): 195 | self._redo_button.remove_value_listener(self._redo_value) 196 | self._redo_button = redo_button 197 | if (self._redo_button != None): 198 | self._redo_button.add_value_listener(self._redo_value) 199 | self.update() 200 | 201 | 202 | #def set_bts_button(self, bts_button): #"back to start" button 203 | #assert isinstance(bts_button, (ButtonElement, 204 | #type(None))) 205 | #if (bts_button != self._bts_button): 206 | #if (self._bts_button != None): 207 | #self._bts_button.remove_value_listener(self._bts_value) 208 | #self._bts_button = bts_button 209 | #if (self._bts_button != None): 210 | #self._bts_button.add_value_listener(self._bts_value) 211 | #self.update() 212 | 213 | 214 | def _undo_value(self, value): 215 | #if self._shift_pressed: #added 216 | assert (self._undo_button != None) 217 | assert (value in range(128)) 218 | if self.is_enabled(): 219 | if ((value != 0) or (not self._undo_button.is_momentary())): 220 | if self.song().can_undo: 221 | self.song().undo() 222 | 223 | 224 | def _redo_value(self, value): 225 | #if self._shift_pressed: #added 226 | assert (self._redo_button != None) 227 | assert (value in range(128)) 228 | if self.is_enabled(): 229 | if ((value != 0) or (not self._redo_button.is_momentary())): 230 | if self.song().can_redo: 231 | self.song().redo() 232 | 233 | 234 | #def _bts_value(self, value): 235 | #assert (self._bts_button != None) 236 | #assert (value in range(128)) 237 | #if self.is_enabled(): 238 | #if ((value != 0) or (not self._bts_button.is_momentary())): 239 | #self.song().current_song_time = 0.0 240 | 241 | 242 | def _tempo_encoder_value(self, value): 243 | ##if not self._shift_pressed: 244 | #if self._shift_pressed: 245 | assert (self._tempo_encoder_control != None) 246 | assert (value in range(128)) 247 | backwards = (value >= 64) 248 | step = 0.1 #step = 1.0 #reduce this for finer control; 1.0 is 1 bpm 249 | if backwards: 250 | amount = (value - 128) 251 | else: 252 | amount = value 253 | tempo = max(20, min(999, (self.song().tempo + (amount * step)))) 254 | self.song().tempo = tempo 255 | 256 | 257 | def set_tempo_encoder(self, control): 258 | assert ((control == None) or (isinstance(control, EncoderElement) and (control.message_map_mode() is Live.MidiMap.MapMode.relative_two_compliment))) 259 | if (self._tempo_encoder_control != None): 260 | self._tempo_encoder_control.remove_value_listener(self._tempo_encoder_value) 261 | self._tempo_encoder_control = control 262 | if (self._tempo_encoder_control != None): 263 | self._tempo_encoder_control.add_value_listener(self._tempo_encoder_value) 264 | self.update() 265 | 266 | @subject_slot('value') 267 | def _tempo_value(self, value): #Override to pull tempo range from MIDI_Maps.py 268 | assert (self._tempo_control != None) 269 | assert (value in range(128)) 270 | if self.is_enabled(): 271 | fraction = ((TEMPO_TOP - TEMPO_BOTTOM) / 127.0) 272 | self.song().tempo = ((fraction * value) + TEMPO_BOTTOM) 273 | 274 | -------------------------------------------------------------------------------- /YourControllerName/SpecialViewControllerComponent.py: -------------------------------------------------------------------------------- 1 | # There are lot of things to explore that you can use to add extra functionality to your scipt. 2 | # You'd essentially enable it here by uncommenting then add it YourControllerName.py and MIDI_Map. 3 | # I experiment more and add myself in the future. 4 | 5 | # Partial --== Decompile ==-- with fixes 6 | import Live 7 | from _Framework.ControlSurfaceComponent import ControlSurfaceComponent 8 | from _Framework.ButtonElement import ButtonElement 9 | SHOW_PLAYING_CLIP_DELAY = 5 10 | class DetailViewControllerComponent(ControlSurfaceComponent): 11 | __module__ = __name__ 12 | __doc__ = ' Component that can toggle the device chain- and clip view of the selected track ' 13 | 14 | def __init__(self): 15 | ControlSurfaceComponent.__init__(self) 16 | self._device_clip_toggle_button = None 17 | self._detail_toggle_button = None 18 | self._left_button = None 19 | self._right_button = None 20 | #self._shift_button = None 21 | #self._shift_pressed = False 22 | self._show_playing_clip_ticks_delay = -1 23 | self.application().view.add_is_view_visible_listener('Detail', self._detail_view_visibility_changed) 24 | self._register_timer_callback(self._on_timer) 25 | return None 26 | 27 | def disconnect(self): 28 | self._unregister_timer_callback(self._on_timer) 29 | self.application().view.remove_is_view_visible_listener('Detail', self._detail_view_visibility_changed) 30 | if self._device_clip_toggle_button != None: 31 | self._device_clip_toggle_button.remove_value_listener(self._device_clip_toggle_value) 32 | self._device_clip_toggle_button = None 33 | if self._detail_toggle_button != None: 34 | self._detail_toggle_button.remove_value_listener(self._detail_toggle_value) 35 | self._detail_toggle_button = None 36 | if self._left_button != None: 37 | self._left_button.remove_value_listener(self._nav_value) 38 | self._left_button = None 39 | if self._right_button != None: 40 | self._right_button.remove_value_listener(self._nav_value) 41 | self._right_button = None 42 | #if self._shift_button != None: 43 | #self._shift_button.remove_value_listener(self._shift_value) 44 | #self._shift_button = None 45 | return None 46 | 47 | def set_device_clip_toggle_button(self, button): 48 | if not(button == None or isinstance(button, ButtonElement)): 49 | isinstance(button, ButtonElement) 50 | raise AssertionError 51 | if self._device_clip_toggle_button != button: 52 | if self._device_clip_toggle_button != None: 53 | self._device_clip_toggle_button.remove_value_listener(self._device_clip_toggle_value) 54 | self._device_clip_toggle_button = button 55 | if self._device_clip_toggle_button != None: 56 | self._device_clip_toggle_button.add_value_listener(self._device_clip_toggle_value) 57 | 58 | self.update() 59 | return None 60 | 61 | def set_detail_toggle_button(self, button): 62 | if not(button == None or isinstance(button, ButtonElement)): 63 | isinstance(button, ButtonElement) 64 | raise AssertionError 65 | if self._detail_toggle_button != button: 66 | if self._detail_toggle_button != None: 67 | self._detail_toggle_button.remove_value_listener(self._detail_toggle_value) 68 | self._detail_toggle_button = button 69 | if self._detail_toggle_button != None: 70 | self._detail_toggle_button.add_value_listener(self._detail_toggle_value) 71 | 72 | self.update() 73 | return None 74 | 75 | def set_device_nav_buttons(self, left_button, right_button): 76 | if not(left_button == None or isinstance(left_button, ButtonElement)): 77 | isinstance(left_button, ButtonElement) 78 | raise AssertionError 79 | if not(right_button == None or isinstance(right_button, ButtonElement)): 80 | isinstance(right_button, ButtonElement) 81 | raise AssertionError 82 | identify_sender = True 83 | if self._left_button != None: 84 | self._left_button.remove_value_listener(self._nav_value) 85 | self._left_button = left_button 86 | if self._left_button != None: 87 | self._left_button.add_value_listener(self._nav_value, identify_sender) 88 | if self._right_button != None: 89 | self._right_button.remove_value_listener(self._nav_value) 90 | self._right_button = right_button 91 | if self._right_button != None: 92 | self._right_button.add_value_listener(self._nav_value, identify_sender) 93 | 94 | self.update() 95 | return None 96 | 97 | #def set_shift_button(self, button): 98 | #if not(button == None or isinstance(button, ButtonElement) and button.is_momentary()): 99 | #isinstance(button, ButtonElement) 100 | #raise AssertionError 101 | #if self._shift_button != button: 102 | #if self._shift_button != None: 103 | #self._shift_button.remove_value_listener(self._shift_value) 104 | #self._shift_button = button 105 | #if self._shift_button != None: 106 | #self._shift_button.add_value_listener(self._shift_value) 107 | # 108 | #self.update() 109 | #return None 110 | 111 | def on_enabled_changed(self): 112 | self.update() 113 | 114 | def update(self): 115 | pass 116 | #if self.is_enabled(): 117 | #self.is_enabled() 118 | #if not self._shift_pressed: 119 | #self._shift_pressed 120 | #if self._left_button != None: 121 | #self._left_button.turn_off() 122 | #if self._right_button != None: 123 | #self._right_button.turn_off() 124 | #if self._device_clip_toggle_button != None: 125 | #self._device_clip_toggle_button.turn_off() 126 | #self._detail_view_visibility_changed() 127 | #else: 128 | #self._shift_pressed 129 | #else: 130 | #self.is_enabled() 131 | return None 132 | 133 | def _detail_view_visibility_changed(self): 134 | if self.is_enabled() and self._detail_toggle_button != None: #not self._shift_pressed and self._detail_toggle_button != None: 135 | if self.application().view.is_view_visible('Detail'): 136 | self.application().view.is_view_visible('Detail') 137 | self._detail_toggle_button.turn_on() 138 | else: 139 | self.application().view.is_view_visible('Detail') 140 | self._detail_toggle_button.turn_off() 141 | else: 142 | self.is_enabled() 143 | return None 144 | 145 | def _device_clip_toggle_value(self, value): 146 | if not self._device_clip_toggle_button != None: 147 | raise AssertionError 148 | if not value in range(128): 149 | raise AssertionError 150 | if self.is_enabled(): #and not self._shift_pressed: 151 | button_is_momentary = self._device_clip_toggle_button.is_momentary() 152 | if not button_is_momentary or value != 0: 153 | not button_is_momentary 154 | if not self.application().view.is_view_visible('Detail'): 155 | self.application().view.is_view_visible('Detail') 156 | self.application().view.show_view('Detail') 157 | else: 158 | self.application().view.is_view_visible('Detail') 159 | if not self.application().view.is_view_visible('Detail/DeviceChain'): 160 | self.application().view.is_view_visible('Detail/DeviceChain') 161 | self.application().view.show_view('Detail/DeviceChain') 162 | else: 163 | self.application().view.is_view_visible('Detail/DeviceChain') 164 | self.application().view.show_view('Detail/Clip') 165 | if button_is_momentary and value != 0: 166 | self._show_playing_clip_ticks_delay = SHOW_PLAYING_CLIP_DELAY 167 | else: 168 | button_is_momentary 169 | self._show_playing_clip_ticks_delay = -1 170 | else: 171 | self.is_enabled() 172 | return None 173 | 174 | 175 | def _detail_toggle_value(self, value): 176 | assert (self._detail_toggle_button != None) 177 | assert (value in range(128)) 178 | if self.is_enabled(): # and (not self._shift_pressed): 179 | if ((not self._detail_toggle_button.is_momentary()) or (value != 0)): 180 | if (not self.application().view.is_view_visible('Detail')): 181 | self.application().view.show_view('Detail') 182 | else: 183 | self.application().view.hide_view('Detail') 184 | 185 | 186 | #def _shift_value(self, value): 187 | #if not self._shift_button != None: 188 | #raise AssertionError 189 | #if not value in range(128): 190 | #raise AssertionError 191 | #self._shift_pressed = value != 0 192 | #self.update() 193 | #return None 194 | 195 | def _nav_value(self, value, sender): 196 | assert ((sender != None) and (sender in (self._left_button, 197 | self._right_button))) 198 | if self.is_enabled(): # and (not self._shift_pressed): 199 | if ((not sender.is_momentary()) or (value != 0)): 200 | modifier_pressed = True 201 | if ((not self.application().view.is_view_visible('Detail')) or (not self.application().view.is_view_visible('Detail/DeviceChain'))): 202 | self.application().view.show_view('Detail') 203 | self.application().view.show_view('Detail/DeviceChain') 204 | else: 205 | direction = Live.Application.Application.View.NavDirection.left 206 | if (sender == self._right_button): 207 | direction = Live.Application.Application.View.NavDirection.right 208 | self.application().view.scroll_view(direction, 'Detail/DeviceChain', (not modifier_pressed)) 209 | 210 | def _on_timer(self): 211 | if self.is_enabled(): # and (not self._shift_pressed): 212 | if (self._show_playing_clip_ticks_delay > -1): 213 | if (self._show_playing_clip_ticks_delay == 0): 214 | song = self.song() 215 | playing_slot_index = song.view.selected_track.playing_slot_index 216 | if (playing_slot_index > -1): 217 | song.view.selected_scene = song.scenes[playing_slot_index] 218 | if song.view.highlighted_clip_slot.has_clip: 219 | self.application().view.show_view('Detail/Clip') 220 | self._show_playing_clip_ticks_delay -= 1 221 | -------------------------------------------------------------------------------- /YourControllerName/SpecialZoomingComponent.py: -------------------------------------------------------------------------------- 1 | # emacs-mode: -*- python-*- 2 | # -*- coding: utf-8 -*- 3 | 4 | import Live 5 | from _Framework.SessionZoomingComponent import SessionZoomingComponent 6 | from _Framework.ButtonElement import ButtonElement 7 | class SpecialZoomingComponent(SessionZoomingComponent): 8 | ' Special ZoomingComponent that uses clip stop buttons for stop all when zoomed ' 9 | __module__ = __name__ 10 | 11 | def __init__(self, session): 12 | SessionZoomingComponent.__init__(self, session) 13 | 14 | 15 | def _scroll_up(self): 16 | #if self._is_zoomed_out: 17 | height = self._session.height() 18 | track_offset = self._session.track_offset() 19 | scene_offset = self._session.scene_offset() 20 | 21 | if scene_offset > 0: 22 | new_scene_offset = scene_offset 23 | if scene_offset % height > 0: 24 | new_scene_offset -= (scene_offset % height) 25 | else: 26 | new_scene_offset = max(0, scene_offset - height) 27 | self._session.set_offsets(track_offset, new_scene_offset) 28 | 29 | def _scroll_down(self): 30 | #if self._is_zoomed_out: 31 | height = self._session.height() 32 | track_offset = self._session.track_offset() 33 | scene_offset = self._session.scene_offset() 34 | new_scene_offset = scene_offset + height - (scene_offset % height) 35 | self._session.set_offsets(track_offset, new_scene_offset) 36 | 37 | def _scroll_left(self): 38 | #if self._is_zoomed_out: 39 | width = self._session.width() 40 | track_offset = self._session.track_offset() 41 | scene_offset = self._session.scene_offset() 42 | if track_offset > 0: 43 | new_track_offset = track_offset 44 | if track_offset % width > 0: 45 | new_track_offset -= (track_offset % width) 46 | else: 47 | new_track_offset = max(0, track_offset - width) 48 | self._session.set_offsets(new_track_offset, scene_offset) 49 | 50 | def _scroll_right(self): 51 | #if self._is_zoomed_out: 52 | width = self._session.width() 53 | track_offset = self._session.track_offset() 54 | scene_offset = self._session.scene_offset() 55 | new_track_offset = track_offset + width - (track_offset % width) 56 | self._session.set_offsets(new_track_offset, scene_offset) 57 | -------------------------------------------------------------------------------- /YourControllerName/YourControllerName.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import Live 4 | from _Framework.ControlSurface import ControlSurface 5 | from _Framework.InputControlElement import * 6 | from _Framework.SliderElement import SliderElement 7 | from _Framework.ButtonElement import ButtonElement 8 | from _Framework.ButtonMatrixElement import ButtonMatrixElement 9 | from _Framework.ChannelStripComponent import ChannelStripComponent 10 | from _Framework.DeviceComponent import DeviceComponent 11 | from _Framework.ControlSurfaceComponent import ControlSurfaceComponent 12 | from _Framework.SessionZoomingComponent import SessionZoomingComponent 13 | from SpecialMixerComponent import SpecialMixerComponent 14 | from SpecialTransportComponent import SpecialTransportComponent 15 | from SpecialSessionComponent import SpecialSessionComponent 16 | from SpecialZoomingComponent import SpecialZoomingComponent 17 | from SpecialViewControllerComponent import DetailViewControllerComponent 18 | from MIDI_Map import * 19 | 20 | 21 | # MIDI_NOTE_TYPE = 0 22 | # MIDI_CC_TYPE = 1 23 | # MIDI_PB_TYPE = 2 24 | 25 | 26 | class YourControllerName(ControlSurface): # Make sure you update the name 27 | __doc__ = " Script for YourControllerName in APC emulation mode " # Make sure you update the name 28 | 29 | _active_instances = [] 30 | 31 | def _combine_active_instances(): 32 | track_offset = 0 33 | scene_offset = 0 34 | for instance in YourControllerName._active_instances: # Make sure you update the name 35 | instance._activate_combination_mode(track_offset, scene_offset) 36 | track_offset += instance._session.width() 37 | _combine_active_instances = staticmethod(_combine_active_instances) 38 | 39 | def __init__(self, c_instance): 40 | ControlSurface.__init__(self, c_instance) 41 | # self.set_suppress_rebuild_requests(True) 42 | with self.component_guard(): 43 | self._note_map = [] 44 | self._ctrl_map = [] 45 | self._load_MIDI_map() 46 | self._session = None 47 | self._session_zoom = None 48 | self._mixer = None 49 | self._setup_session_control() 50 | self._setup_mixer_control() 51 | self._session.set_mixer(self._mixer) 52 | self._setup_device_and_transport_control() 53 | self.set_highlighting_session_component(self._session) 54 | # self.set_suppress_rebuild_requests(False) 55 | self._pads = [] 56 | self._load_pad_translations() 57 | self._do_combine() 58 | 59 | def disconnect(self): 60 | self._note_map = None 61 | self._ctrl_map = None 62 | self._pads = None 63 | self._do_uncombine() 64 | self._shift_button = None 65 | self._session = None 66 | self._session_zoom = None 67 | self._mixer = None 68 | ControlSurface.disconnect(self) 69 | 70 | def _do_combine(self): 71 | if self not in YourControllerName._active_instances: # Make sure you update the name 72 | YourControllerName._active_instances.append(self) # Make sure you update the name 73 | YourControllerName._combine_active_instances() # Make sure you update the name 74 | 75 | def _do_uncombine(self): 76 | if (self in YourControllerName._active_instances) and YourControllerName._active_instances.remove(self): # Make sure you update the name 77 | self._session.unlink() 78 | YourControllerName._combine_active_instances() # Make sure you update the name 79 | 80 | def _activate_combination_mode(self, track_offset, scene_offset): 81 | if TRACK_OFFSET != -1: 82 | track_offset = TRACK_OFFSET 83 | if SCENE_OFFSET != -1: 84 | scene_offset = SCENE_OFFSET 85 | self._session.link_with_track_offset(track_offset, scene_offset) 86 | 87 | def _setup_session_control(self): 88 | is_momentary = True 89 | self._session = SpecialSessionComponent(TSB_X, TSB_Y) # Track selection box size (X,Y) (horizontal, vertical). 90 | self._session.name = 'Session_Control' 91 | self._session.set_track_bank_buttons(self._note_map[SESSIONRIGHT], self._note_map[SESSIONLEFT]) 92 | self._session.set_scene_bank_buttons(self._note_map[SESSIONDOWN], self._note_map[SESSIONUP]) 93 | self._session.set_select_buttons(self._note_map[SCENEDN], self._note_map[SCENEUP]) 94 | # range(tsb_x) is the horizontal count for the track selection box 95 | self._scene_launch_buttons = [self._note_map[SCENELAUNCH[index]] for index in range(TSB_X)] 96 | # range(tsb_y) Range value is the track selection 97 | self._track_stop_buttons = [self._note_map[TRACKSTOP[index]] for index in range(TSB_Y)] 98 | self._session.set_stop_all_clips_button(self._note_map[STOPALLCLIPS]) 99 | self._session.set_stop_track_clip_buttons(tuple(self._track_stop_buttons)) 100 | self._session.selected_scene().name = 'Selected_Scene' 101 | self._session.selected_scene().set_launch_button(self._note_map[SELSCENELAUNCH]) 102 | self._session.set_slot_launch_button(self._note_map[SELCLIPLAUNCH]) 103 | for scene_index in range(TSB_Y): # Change range() value to set the vertical count for track selection box 104 | scene = self._session.scene(scene_index) 105 | scene.name = 'Scene_' + str(scene_index) 106 | button_row = [] 107 | scene.set_launch_button(self._scene_launch_buttons[scene_index]) 108 | scene.set_triggered_value(2) 109 | for track_index in range(TSB_X): # Change range() value to set the horizontal count for track selection box 110 | button = self._note_map[CLIPNOTEMAP[scene_index][track_index]] 111 | button_row.append(button) 112 | clip_slot = scene.clip_slot(track_index) 113 | clip_slot.name = str(track_index) + '_Clip_Slot_' + str(scene_index) 114 | clip_slot.set_launch_button(button) 115 | self._session_zoom = SpecialZoomingComponent(self._session) 116 | self._session_zoom.name = 'Session_Overview' 117 | self._session_zoom.set_nav_buttons(self._note_map[ZOOMUP], self._note_map[ZOOMDOWN], self._note_map[ZOOMLEFT], self._note_map[ZOOMRIGHT]) 118 | 119 | def _setup_mixer_control(self): 120 | 121 | is_momentary = True 122 | self._mixer = SpecialMixerComponent(8) 123 | self._mixer.name = 'Mixer' 124 | self._mixer.master_strip().name = 'Master_Channel_Strip' 125 | self._mixer.master_strip().set_select_button(self._note_map[MASTERSEL]) 126 | self._mixer.selected_strip().name = 'Selected_Channel_Strip' 127 | self._mixer.set_select_buttons(self._note_map[TRACKRIGHT], self._note_map[TRACKLEFT]) 128 | self._mixer.set_crossfader_control(self._ctrl_map[CROSSFADER]) 129 | self._mixer.set_prehear_volume_control(self._ctrl_map[CUELEVEL]) 130 | self._mixer.master_strip().set_volume_control(self._ctrl_map[MASTERVOLUME]) 131 | self._mixer.selected_strip().set_arm_button(self._note_map[SELTRACKREC]) 132 | self._mixer.selected_strip().set_solo_button(self._note_map[SELTRACKSOLO]) 133 | self._mixer.selected_strip().set_mute_button(self._note_map[SELTRACKMUTE]) 134 | for track in range(8): 135 | # My guess is that altering the range here will allow you to alter the range of mixer tracks 136 | # So if you had a 16 fader mixer, this would come in handy. 137 | strip = self._mixer.channel_strip(track) 138 | strip.name = 'Channel_Strip_' + str(track) 139 | strip.set_arm_button(self._note_map[TRACKREC[track]]) 140 | strip.set_solo_button(self._note_map[TRACKSOLO[track]]) 141 | strip.set_mute_button(self._note_map[TRACKMUTE[track]]) 142 | strip.set_select_button(self._note_map[TRACKSEL[track]]) 143 | strip.set_volume_control(self._ctrl_map[TRACKVOL[track]]) 144 | strip.set_pan_control(self._ctrl_map[TRACKPAN[track]]) 145 | strip.set_send_controls((self._ctrl_map[TRACKSENDA[track]], self._ctrl_map[TRACKSENDB[track]], self._ctrl_map[TRACKSENDC[track]])) 146 | strip.set_invert_mute_feedback(True) 147 | 148 | def _setup_device_and_transport_control(self): 149 | is_momentary = True 150 | self._device = DeviceComponent() 151 | self._device.name = 'Device_Component' 152 | device_bank_buttons = [] 153 | device_param_controls = [] 154 | for index in range(8): 155 | device_param_controls.append(self._ctrl_map[PARAMCONTROL[index]]) 156 | device_bank_buttons.append(self._note_map[DEVICEBANK[index]]) 157 | if None not in device_bank_buttons: 158 | self._device.set_bank_buttons(tuple(device_bank_buttons)) 159 | if None not in device_param_controls: 160 | self._device.set_parameter_controls(tuple(device_param_controls)) 161 | self._device.set_on_off_button(self._note_map[DEVICEONOFF]) 162 | self._device.set_bank_nav_buttons(self._note_map[DEVICEBANKNAVLEFT], self._note_map[DEVICEBANKNAVRIGHT]) 163 | self._device.set_lock_button(self._note_map[DEVICELOCK]) 164 | self.set_device_component(self._device) 165 | 166 | detail_view_toggler = DetailViewControllerComponent() 167 | detail_view_toggler.name = 'Detail_View_Control' 168 | detail_view_toggler.set_device_clip_toggle_button(self._note_map[CLIPTRACKVIEW]) 169 | detail_view_toggler.set_detail_toggle_button(self._note_map[DETAILVIEW]) 170 | detail_view_toggler.set_device_nav_buttons(self._note_map[DEVICENAVLEFT], self._note_map[DEVICENAVRIGHT]) 171 | 172 | transport = SpecialTransportComponent() 173 | transport.name = 'Transport' 174 | transport.set_play_button(self._note_map[PLAY]) 175 | transport.set_stop_button(self._note_map[STOP]) 176 | transport.set_record_button(self._note_map[REC]) 177 | transport.set_nudge_buttons(self._note_map[NUDGEUP], self._note_map[NUDGEDOWN]) 178 | transport.set_undo_button(self._note_map[UNDO]) 179 | transport.set_redo_button(self._note_map[REDO]) 180 | transport.set_tap_tempo_button(self._note_map[TAPTEMPO]) 181 | transport.set_quant_toggle_button(self._note_map[RECQUANT]) 182 | transport.set_overdub_button(self._note_map[OVERDUB]) 183 | transport.set_metronome_button(self._note_map[METRONOME]) 184 | transport.set_tempo_control(self._ctrl_map[TEMPOCONTROL]) 185 | transport.set_loop_button(self._note_map[LOOP]) 186 | transport.set_seek_buttons(self._note_map[SEEKFWD], self._note_map[SEEKRWD]) 187 | transport.set_punch_buttons(self._note_map[PUNCHIN], self._note_map[PUNCHOUT]) 188 | # transport.set_song_position_control(self._ctrl_map[SONGPOSITION]) #still not implemented as of Live 8.1.6 189 | 190 | def _on_selected_track_changed(self): 191 | ControlSurface._on_selected_track_changed(self) 192 | track = self.song().view.selected_track 193 | device_to_select = track.view.selected_device 194 | if device_to_select is None and len(track.devices) > 0: 195 | device_to_select = track.devices[0] 196 | if device_to_select is not None: 197 | self.song().view.select_device(device_to_select) 198 | self._device_component.set_device(device_to_select) 199 | 200 | def _load_pad_translations(self): 201 | if -1 not in DRUM_PADS: 202 | pad = [] 203 | for row in range(4): 204 | for col in range(4): 205 | pad = (col, row, DRUM_PADS[row*4 + col], PADCHANNEL,) 206 | self._pads.append(pad) 207 | self.set_pad_translations(tuple(self._pads)) 208 | 209 | def _load_MIDI_map(self): 210 | is_momentary = True 211 | for note in range(128): 212 | button = ButtonElement(is_momentary, MESSAGETYPE, BUTTONCHANNEL, note) 213 | button.name = 'Note_' + str(note) 214 | self._note_map.append(button) 215 | self._note_map.append(None) # add None to the end of the list, selectable with [-1] 216 | if MESSAGETYPE == MIDI_CC_TYPE and BUTTONCHANNEL == SLIDERCHANNEL: 217 | for ctrl in range(128): 218 | self._ctrl_map.append(None) 219 | else: 220 | for ctrl in range(128): 221 | control = SliderElement(MIDI_CC_TYPE, SLIDERCHANNEL, ctrl) 222 | control.name = 'Ctrl_' + str(ctrl) 223 | self._ctrl_map.append(control) 224 | self._ctrl_map.append(None) 225 | -------------------------------------------------------------------------------- /YourControllerName/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import Live 3 | from YourControllerName import YourControllerName 4 | 5 | def create_instance(c_instance): 6 | ' Creates and returns the APC20 script ' 7 | return YourControllerName(c_instance) 8 | 9 | 10 | # local variables: 11 | # tab-width: 4 12 | --------------------------------------------------------------------------------