├── bvalosek_Midi_Fighter_Twister ├── __init__.py ├── consts.py ├── ModesComponentEx.py ├── SliderElementEx.py ├── SkinDefault.py ├── ButtonElementEx.py ├── Colors.py ├── BackgroundComponent.py ├── MenuComponent.py ├── TwisterControlSurface.py └── DeviceComponentEx.py ├── .gitignore ├── LICENSE ├── HISTORY.md └── README.md /bvalosek_Midi_Fighter_Twister/__init__.py: -------------------------------------------------------------------------------- 1 | from TwisterControlSurface import TwisterControlSurface 2 | 3 | def create_instance(c_instance): 4 | return TwisterControlSurface(c_instance) 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore all (stock) controller scripts 2 | /* 3 | 4 | # but not our stuff 5 | !bvalosek* 6 | !README.md 7 | !HISTORY.md 8 | !LICENSE 9 | !.gitignore 10 | 11 | # but do ignore compiled files 12 | *.pyc 13 | -------------------------------------------------------------------------------- /bvalosek_Midi_Fighter_Twister/consts.py: -------------------------------------------------------------------------------- 1 | # encoder controls 2 | KNOB_CHANNEL = 0 3 | 4 | # button controls (send to set light color) 5 | BUTTON_CHANNEL = 1 6 | 7 | # animation setting channel for the buttons / lights 8 | BUTTON_ANIMATION_CHANNEL = 2 9 | 10 | # animation setting channel for the encoder rings 11 | KNOB_ANIMATION_CHANNEL = 5 12 | 13 | -------------------------------------------------------------------------------- /bvalosek_Midi_Fighter_Twister/ModesComponentEx.py: -------------------------------------------------------------------------------- 1 | from _Framework.ModesComponent import ModesComponent 2 | 3 | class ModesComponentEx(ModesComponent): 4 | """ 5 | A special ModesComponent for the twister that lets us skin the mode buttons 6 | """ 7 | 8 | def set_mode_button(self, name, button): 9 | if button: 10 | button.set_on_off_values('Modes.Selected', 'Modes.NotSelected') 11 | super(ModesComponentEx, self).set_mode_button(name, button) 12 | -------------------------------------------------------------------------------- /bvalosek_Midi_Fighter_Twister/SliderElementEx.py: -------------------------------------------------------------------------------- 1 | from _Framework.SliderElement import SliderElement 2 | 3 | from consts import * 4 | 5 | class SliderElementEx(SliderElement): 6 | """ 7 | A special SliderElement that handles setting the animation and brightness 8 | of the encoder rings when connecting / disconnecting 9 | """ 10 | 11 | def connect_to(self, param): 12 | super(SliderElementEx, self).connect_to(param) 13 | self.send_value(95, channel = KNOB_ANIMATION_CHANNEL, force = True) 14 | 15 | def release_parameter(self, *a, **k): 16 | super(SliderElementEx, self).release_parameter(*a, **k) 17 | self.send_value(0, force = True) 18 | self.send_value(65, channel = KNOB_ANIMATION_CHANNEL, force = True) 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-2017 Brandon Valosek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Change History 2 | 3 | ## v3.0.0 (???) 4 | 5 | * Dropping everything but Midi Fighter Twister, starting a new approach 6 | 7 | ## v2.6.0 (2017-01-15) 8 | 9 | * MF Twister Return Select buttons can be pressed again to return to previous non-return track 10 | * MF Twister skinning improvements for non-armable tracks 11 | 12 | ## v2.5.0 (2017-01-14) 13 | 14 | * MPK249 and VI49 set to use Loop button as Tap Tempo 15 | 16 | ## v2.4.0 (2017-01-14) 17 | 18 | * Twister Main Mode shows metronome lights 19 | * Small improvements and cleanup to MF Twister script 20 | 21 | ## v2.3.0 (2017-01-11) 22 | 23 | * Added MPK249 script 24 | * MPK249 and Alesis VI49 record button acts as Push-style session record 25 | 26 | ## v2.2.0 (2017-01-10) 27 | 28 | * Substantial changes to MF Twister scripts, still heavily in flux 29 | * Added transport controls to Alesis VI 49 script 30 | 31 | ## v2.1.0 (2017-01-02) 32 | 33 | * Added Alesis VI 49 script 34 | * Knobs 1-8 control current device parameters 35 | * Knobs 9-12 control selected track sends 36 | * README updates 37 | 38 | ## v2.0.0 (2017-01-02) 39 | 40 | * New approach / redesign based on usage over the last year+ 41 | * Currently only scripts MF Twister 42 | 43 | ## v1.0.0 (2015-07-22) 44 | 45 | * First release 46 | -------------------------------------------------------------------------------- /bvalosek_Midi_Fighter_Twister/SkinDefault.py: -------------------------------------------------------------------------------- 1 | from _Framework.ButtonElement import Color 2 | from _Framework.Skin import Skin 3 | 4 | from Colors import * 5 | 6 | class Colors: 7 | class Modes: 8 | Selected = ColorEx(Rgb.GREEN, Animation.PULSE_1_BEAT) 9 | NotSelected = ColorEx(Rgb.GREEN, Brightness.LOW) 10 | 11 | class DefaultButton: 12 | On = ColorEx(Rgb.GREEN) 13 | Disabled = ColorEx(Rgb.GREEN, Brightness.LOW) 14 | Off = ColorEx(Rgb.OFF, Brightness.OFF) 15 | 16 | class Device: 17 | Lock = ColorEx(Rgb.BLUE, Brightness.LOW) 18 | LockOffset = ColorEx(Rgb.ORANGE, Brightness.LOW) 19 | 20 | Unlock = ColorEx(Rgb.RED, Animation.GATE_HALF_BEAT) 21 | NormalParams = ColorEx(Rgb.BLUE, Animation.GATE_HALF_BEAT) 22 | OffsetParams = ColorEx(Rgb.ORANGE, Animation.GATE_QUARTER_BEAT) 23 | Select = ColorEx(Rgb.TEAL, Animation.GATE_HALF_BEAT) 24 | MenuActive = ColorEx(Rgb.PURPLE, Animation.GATE_QUARTER_BEAT) 25 | 26 | HalfSnap = ColorEx(Rgb.BLUE, Animation.GATE_HALF_BEAT) 27 | ReverseHalfSnap = ColorEx(Rgb.GREEN, Animation.GATE_HALF_BEAT) 28 | FullSnap = ColorEx(Rgb.PURPLE, Animation.GATE_HALF_BEAT) 29 | 30 | def make_default_skin(): 31 | return Skin(Colors) 32 | 33 | -------------------------------------------------------------------------------- /bvalosek_Midi_Fighter_Twister/ButtonElementEx.py: -------------------------------------------------------------------------------- 1 | from _Framework.ButtonElement import ButtonElement, ON_VALUE, OFF_VALUE 2 | 3 | class ButtonElementEx(ButtonElement): 4 | """ 5 | A special type of ButtonElement that allows skinning (that can be 6 | overridden when taking control) 7 | """ 8 | 9 | default_states = { True: 'DefaultButton.On', False: 'DefaultButton.Disabled' } 10 | 11 | def __init__(self, default_states = None, *a, **k): 12 | super(ButtonElementEx, self).__init__(*a, **k) 13 | if default_states is not None: 14 | self.default_states = default_states 15 | self.states = dict(self.default_states) 16 | 17 | def set_on_off_values(self, on = None, off = None): 18 | self.states[True] = on or self.default_states[True] 19 | self.states[False] = off or self.default_states[False] 20 | 21 | def set_light(self, value): 22 | value = self.states.get(value, value) 23 | super(ButtonElementEx, self).set_light(value) 24 | 25 | def send_color(self, color): 26 | color.draw(self) 27 | 28 | def send_value(self, value, **k): 29 | if value is ON_VALUE: 30 | self.set_light(True) 31 | elif value is OFF_VALUE: 32 | self.set_light(False) 33 | else: 34 | super(ButtonElementEx, self).send_value(value, **k) 35 | 36 | -------------------------------------------------------------------------------- /bvalosek_Midi_Fighter_Twister/Colors.py: -------------------------------------------------------------------------------- 1 | from _Framework.ButtonElement import Color 2 | 3 | from consts import * 4 | 5 | class Rgb: 6 | OFF = 0 7 | BLUE = 1 8 | AZURE = 10 9 | TEAL = 20 10 | MINT = 40 11 | GREEN = 52 12 | YELLOW = 61 13 | ORANGE = 68 14 | RED = 85 15 | PINK_RED = 93 16 | PINK = 100 17 | FUCHSIA = 111 18 | PURPLE = 115 19 | 20 | class Animation: 21 | NONE = 0 22 | 23 | GATE_8_BEATS = 1 24 | GATE_4_BEATS = 2 25 | GATE_2_BEATS = 3 26 | GATE_1_BEAT = 4 27 | GATE_HALF_BEAT = 5 28 | GATE_QUARTER_BEAT = 6 29 | GATE_EIGHTH_BEAT = 7 30 | GATE_SIXTEENTH_BEAT = 8 31 | 32 | PULSE_8_BEATS = 10 33 | PULSE_4_BEATS = 11 34 | PULSE_2_BEATS = 12 35 | PULSE_1_BEAT = 13 36 | PULSE_HALF_BEAT = 14 37 | PULSE_QUARTER_BEAT = 15 38 | PULSE_EIGHTH_BEAT = 16 39 | 40 | RAINBOW = 127 41 | 42 | class Brightness: 43 | OFF = 17 44 | MIN = 18 45 | LOW = 25 46 | MID = 32 47 | MAX = 47 48 | 49 | class ColorEx(Color): 50 | def __init__(self, midi_value = Rgb.BLUE, animation = Animation.NONE, *a, **k): 51 | super(ColorEx, self).__init__(midi_value, *a, **k) 52 | self._animation = animation 53 | 54 | def draw(self, interface): 55 | interface.send_value(self.midi_value, channel = BUTTON_CHANNEL, force = True) 56 | interface.send_value(self._animation, channel = BUTTON_ANIMATION_CHANNEL, force = True) 57 | -------------------------------------------------------------------------------- /bvalosek_Midi_Fighter_Twister/BackgroundComponent.py: -------------------------------------------------------------------------------- 1 | from _Framework.ControlSurfaceComponent import ControlSurfaceComponent 2 | 3 | from consts import * 4 | 5 | class BackgroundComponent(ControlSurfaceComponent): 6 | """ 7 | A nop component that we just clear everything. Set to a low-priority layer 8 | so that anything not mapped will get grabbed and cleared 9 | 10 | The buttons are not actually grabbed, just set_light / send_value push out 11 | to them, so other layers can actually grab them 12 | """ 13 | 14 | def __init__(self, raw = None, color = 'DefaultButton.Off', *a, **k): 15 | super(BackgroundComponent, self).__init__(*a, **k) 16 | self._color = color 17 | self._raw = raw 18 | 19 | self._lights = None 20 | self._knobs = None 21 | 22 | def set_raw(self, raw): 23 | self._raw = raw 24 | self.update() 25 | 26 | def set_lights(self, lights): 27 | self._lights = lights 28 | self.update() 29 | 30 | def set_knobs(self, knobs): 31 | self._knobs = knobs 32 | self.update() 33 | 34 | def on_enabled_changed(self): 35 | self.update() 36 | 37 | def update(self): 38 | if self.is_enabled(): 39 | for index, light in enumerate(self._lights or [ ]): 40 | if light: 41 | if self._raw: 42 | self._raw[index].draw(light) 43 | else: 44 | light.set_light(self._color) 45 | for knob in self._knobs or []: 46 | if knob: 47 | knob.send_value(0, force = True) 48 | knob.send_value(65, channel = KNOB_ANIMATION_CHANNEL, force = True) 49 | -------------------------------------------------------------------------------- /bvalosek_Midi_Fighter_Twister/MenuComponent.py: -------------------------------------------------------------------------------- 1 | from _Framework.ControlSurfaceComponent import ControlSurfaceComponent 2 | from _Framework.SubjectSlot import subject_slot_group, subject_slot 3 | 4 | from Colors import * 5 | 6 | OFF_COLOR = ColorEx(Rgb.OFF, Brightness.OFF) 7 | 8 | class MenuComponent(ControlSurfaceComponent): 9 | """ 10 | A component that allows for a set of buttons to be grabbed and trigger 11 | callbacks when pressed 12 | """ 13 | 14 | def __init__(self, enable_lights = True, actions = None, *a, **k): 15 | super(MenuComponent, self).__init__(*a, **k) 16 | 17 | self._enable_lights = enable_lights 18 | self._actions = actions 19 | self._buttons = None 20 | 21 | def set_buttons(self, buttons): 22 | self._buttons = buttons 23 | self.update() 24 | 25 | def on_enabled_changed(self): 26 | self.update() 27 | 28 | def update_action(self, index, action): 29 | self._actions[index] = action 30 | self.update() 31 | 32 | def update_action_color(self, index, color): 33 | self._actions[index][0] = color 34 | self.update() 35 | 36 | @subject_slot_group('value') 37 | def _on_button(self, value, button): 38 | idx = [b for b in self._buttons].index(button) 39 | if len(self._actions) > idx: 40 | _ , down, up = self._actions[idx] 41 | if value and down: 42 | down() 43 | elif not value and up: 44 | up() 45 | 46 | def update(self): 47 | if self.is_enabled(): 48 | self._on_button.replace_subjects(self._buttons or [ ]) 49 | if self._enable_lights: 50 | for action, button in zip(self._actions or [ ], self._buttons or [ ]): 51 | if button: 52 | color, _, _ = action 53 | if color: 54 | button.set_light(color) 55 | else: 56 | OFF_COLOR.draw(button) 57 | else: 58 | self._on_button.replace_subjects([ ]) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MIDI Remote Scripts for Ableton Live 9 2 | 3 | My personal MIDI Remote Scripts for Ableton 9 (tested on Ableton Live Suite 4 | 9.7.2) 5 | 6 | > I frequently iterate on this repo as my gear / setup changes. See the [v2 7 | > iteration circa early 8 | > 2017](https://github.com/bvalosek/ableton-live-scripts/tree/v2.6.0) or the 9 | > [v1 iteration circa 10 | > 2015](https://github.com/bvalosek/ableton-live-scripts/tree/v1.0.0) for 11 | > previous incarnations of my custom scripts 12 | 13 | ## Devices 14 | 15 | * DJ TechTools Midi Fighter Twister 16 | 17 | ## Installation 18 | 19 | * Download a release from [latest releases](https://github.com/bvalosek/ableton-live-scripts/releases) 20 | (most recent version [here](https://github.com/bvalosek/ableton-live-scripts/releases/latest)) 21 | * Copy **ALL** directories prefixed with `bvalosek_` into the MIDI Remote Scripts 22 | directory for Ableton Live (see the [official documentation](https://www.ableton.com/en/help/article/install-third-party-remote-script/)) 23 | * You may need to restart Ableton or at least reload your current Set to see 24 | the newly added scripts 25 | 26 | > You should copy ALL directories even if you only have a specific controller, 27 | > as code is shared between the scripts. 28 | 29 | Alternatively, if you are familiar with git and want to run the absolute latest 30 | (potentially unreleased) code, you could clone this repo right into the MIDI 31 | Remote Scripts directory: 32 | 33 | ```bash 34 | # in your MIDI Remote Scripts directory 35 | $ git init 36 | $ git remote add origin git@github.com:bvalosek/ableton-live-scripts.git 37 | $ git fetch 38 | $ git checkout -t origin/master 39 | ``` 40 | 41 | ## Setup 42 | 43 | Once the scripts are installed via the above instructions, open Live and go to 44 | Preferences -> Link / MIDI and set the Control Surface, Input, and Output 45 | sections for the corresponding controllers you are using. 46 | 47 | For example, for the DJTT MIDI Fighter Twister: 48 | 49 | * Select `bvalosek Midi Fighter Twister` for the Control Surface 50 | * Select `Midi Fighter Twister` for the input 51 | * Select `Midi Fighter Twister` for the output 52 | 53 | All of the Control Surface scripts in this repo will be prefixed with 54 | `bvalosek`. 55 | 56 | Selecting input/output `track`, `sync`, and `remote` are not required to get 57 | the custom scripts working, although you'll likely want to select some of these 58 | depending on how you're using your hardware. 59 | 60 | ## Controllers 61 | 62 | Detailed information about each controller's customization 63 | 64 | ### DJ TechTools Midi Fighter Twister 65 | 66 | > Make sure to update your Twister to the latest firmware via the [Midi Fighter 67 | > Utility 68 | > app](https://store.djtechtools.com/products/midi-fighter-twister#downloads_and_support) 69 | > from DJ Tech Tools 70 | 71 | ## License 72 | 73 | [MIT](https://github.com/bvalosek/ableton-live-scripts/blob/master/LICENSE) 74 | 75 | -------------------------------------------------------------------------------- /bvalosek_Midi_Fighter_Twister/TwisterControlSurface.py: -------------------------------------------------------------------------------- 1 | from _Framework.ButtonMatrixElement import ButtonMatrixElement 2 | from _Framework.ControlSurface import ControlSurface 3 | from _Framework.InputControlElement import MIDI_CC_TYPE 4 | from _Framework.Layer import Layer 5 | from _Framework.ModesComponent import LayerMode 6 | 7 | from consts import * 8 | from Colors import * 9 | 10 | from BackgroundComponent import BackgroundComponent 11 | from ButtonElementEx import ButtonElementEx 12 | from DeviceComponentEx import DeviceComponentEx 13 | from ModesComponentEx import ModesComponentEx 14 | from SkinDefault import make_default_skin 15 | from SliderElementEx import SliderElementEx 16 | 17 | class TwisterControlSurface(ControlSurface): 18 | """ 19 | Custom control for the DJ Tech Tools Midi Fighter Twister controller 20 | """ 21 | 22 | def __init__(self, c_instance): 23 | ControlSurface.__init__(self, c_instance) 24 | 25 | with self.component_guard(): 26 | self._skin = make_default_skin() 27 | self._setup_controls() 28 | self._setup_background() 29 | self._setup_modes() 30 | 31 | def _setup_background(self): 32 | background = BackgroundComponent() 33 | background.layer = Layer(priority = -100, knobs = self._knobs, lights = self._buttons) 34 | 35 | def _setup_controls(self): 36 | knobs = [ [ self._make_knob(row, col) for col in range(4) ] for row in range(4) ] 37 | buttons = [ [ self._make_button(row, col) for col in range(4) ] for row in range(4) ] 38 | self._knobs = ButtonMatrixElement(knobs) 39 | self._buttons = ButtonMatrixElement(buttons) 40 | 41 | def _make_knob(self, row, col): 42 | return SliderElementEx( 43 | msg_type = MIDI_CC_TYPE, 44 | channel = KNOB_CHANNEL, 45 | identifier = row * 4 + col) 46 | 47 | def _make_button(self, row, col): 48 | return ButtonElementEx( 49 | msg_type = MIDI_CC_TYPE, 50 | channel = BUTTON_CHANNEL, 51 | identifier = row * 4 + col, 52 | is_momentary = True, 53 | skin = self._skin) 54 | 55 | def _setup_modes(self): 56 | self._modes = ModesComponentEx() 57 | mappings = dict() 58 | for n in range(4): 59 | self._create_page(n) 60 | mappings["page{}_mode_button".format(n + 1)] = self._buttons.get_button(n, 0) 61 | self._modes.layer = Layer(priority = 10, **mappings) 62 | self._modes.selected_mode = 'page1_mode' 63 | 64 | def _create_page(self, index): 65 | page_num = index + 1 66 | mode_name = "page{}_mode".format(page_num) 67 | msg = lambda: self.show_message("Switched to page {}".format(page_num)) 68 | 69 | devices = [ DeviceComponentEx( 70 | schedule_message = self.schedule_message, 71 | top_buttons = self._buttons.submatrix[:, 0], 72 | log = self.log_message) for n in range(3) ] 73 | 74 | layers = [ Layer( 75 | knobs = self._knobs.submatrix[:, n + 1], 76 | buttons = self._buttons.submatrix[:, n + 1]) for n in range (3) ] 77 | 78 | modes = [ LayerMode(devices[n], layers[n]) for n in range(3) ] 79 | 80 | self._modes.add_mode(mode_name, modes + [ msg ]) 81 | 82 | -------------------------------------------------------------------------------- /bvalosek_Midi_Fighter_Twister/DeviceComponentEx.py: -------------------------------------------------------------------------------- 1 | from _Framework.CompoundComponent import CompoundComponent 2 | from _Framework.DeviceComponent import DeviceComponent 3 | from _Framework.Layer import Layer 4 | from _Framework.ModesComponent import LayerMode, ComponentMode 5 | from _Framework.ModesComponent import ModesComponent 6 | from _Framework.SubjectSlot import subject_slot_group, subject_slot 7 | from ableton.v2.base import liveobj_valid 8 | 9 | from random import randint 10 | 11 | from BackgroundComponent import BackgroundComponent 12 | from Colors import * 13 | from MenuComponent import MenuComponent 14 | 15 | class SnapModes: 16 | HALF = 'Device.HalfSnap' 17 | REVERSE_HALF = 'Device.ReverseHalfSnap' 18 | FULL = 'Device.FullSnap' 19 | 20 | class _DeviceComponent(DeviceComponent): 21 | def __init__(self, log = None, *a, **k): 22 | super(_DeviceComponent, self).__init__(*a, **k) 23 | self.log = log 24 | self._param_offset = False 25 | 26 | def set_param_offset(self, value): 27 | self._param_offset = value 28 | self.update() 29 | 30 | def toggle_param_offset(self): 31 | self.set_param_offset(not self._param_offset) 32 | 33 | def _current_bank_details(self): 34 | """ Override default behavior to factor in param_offset """ 35 | bank_name, bank = super(_DeviceComponent, self)._current_bank_details() 36 | if bank and len(bank) > 4 and self._param_offset: 37 | bank = bank[4:] 38 | return (bank_name, bank) 39 | 40 | def get_parameter(self, idx = 0): 41 | _, bank = self._current_bank_details() 42 | if idx >= len(bank): return None 43 | return bank[idx] 44 | 45 | 46 | class DeviceComponentEx(CompoundComponent): 47 | """ 48 | Extended DeviceComponent for the Midi Fighter Twister 49 | """ 50 | 51 | next_color = 1 52 | 53 | def __init__(self, schedule_message, log = None, top_buttons = None, *a, **k): 54 | super(DeviceComponentEx, self).__init__(*a, **k) 55 | self.log = log 56 | self.schedule_message = schedule_message 57 | 58 | self._knobs = None 59 | self._buttons = None 60 | self._top_buttons = top_buttons 61 | 62 | self._snap_modes = [ SnapModes.REVERSE_HALF ] * 8 63 | 64 | self._param_values = [ None ] * 8 65 | self._param_down_values = [ None ] * 8 66 | 67 | self._setup_background() 68 | self._setup_device() 69 | self._setup_empty_menu() 70 | self._setup_device_menu() 71 | self._setup_active_menu() 72 | self._setup_top_menu() 73 | self._setup_modes() 74 | 75 | def _setup_empty_menu(self): 76 | actions = [ 77 | ('Device.Lock', self._lock_device, None), 78 | ('Device.LockOffset', lambda: self._lock_device(True), None), 79 | (None, None, None), 80 | (None, None, None) ] 81 | self._empty = self.register_component(MenuComponent( 82 | actions = actions, 83 | is_enabled = False)) 84 | 85 | def _setup_device_menu(self): 86 | fn = lambda n, v: lambda: self._on_param(n, v) 87 | actions = [ 88 | (None, fn(n, True), fn(n, False)) for n in range(3) ] + [ 89 | (None, lambda: self._modes.push_mode('menu'), None) ] 90 | self._device_buttons = self.register_component(MenuComponent( 91 | actions = actions, 92 | enable_lights = False, 93 | is_enabled = False)) 94 | 95 | def _setup_active_menu(self): 96 | actions = [ 97 | ('Device.Unlock', lambda: self._unlock_device(), None), 98 | ('Device.NormalParams', lambda: self._toggle_param_offset(), None), 99 | ('Device.Select', lambda: self._select_device(), None), 100 | ('Device.MenuActive', None, lambda: self._modes.pop_mode('menu')) ] 101 | self._menu = self.register_component(MenuComponent( 102 | actions = actions, 103 | is_enabled = False)) 104 | 105 | def _setup_top_menu(self): 106 | fn = lambda n: lambda: self._on_toggle_snap_mode(n) 107 | actions = [ 108 | (self._snap_modes[n], fn(n), None) for n in range(3) ] + [ 109 | ('DefaultButton.Off', None, None) ] 110 | self._top_menu = self.register_component(MenuComponent( 111 | layer = Layer(priority = 20, buttons = self._top_buttons), 112 | actions = actions, 113 | is_enabled = False)) 114 | 115 | def _setup_background(self): 116 | self._background = self.register_component(BackgroundComponent( 117 | is_enabled = False)) 118 | color = DeviceComponentEx.next_color 119 | DeviceComponentEx.next_color = (color + 31) % 127 120 | self._background.set_raw([ ColorEx(color) for n in range(4) ]) 121 | 122 | def _setup_device(self): 123 | self._device = self.register_component(_DeviceComponent( 124 | log = self.log, 125 | is_enabled = False)) 126 | 127 | def _setup_modes(self): 128 | self._modes = self.register_component(ModesComponent()) 129 | self._modes.add_mode('empty', [ ComponentMode(self._empty) ]) 130 | self._modes.add_mode('device', [ 131 | ComponentMode(self._device_buttons), 132 | ComponentMode(self._device), 133 | ComponentMode(self._background) ]) 134 | self._modes.add_mode('menu', [ 135 | ComponentMode(self._top_menu), 136 | ComponentMode(self._menu) ]) 137 | self._modes.selected_mode = 'empty'; 138 | 139 | def set_knobs(self, knobs): 140 | self._knobs = knobs 141 | self._device.set_parameter_controls(knobs) 142 | self.update(); 143 | 144 | def set_buttons(self, buttons): 145 | self._buttons = buttons 146 | self._background.set_lights(buttons) 147 | self._empty.set_buttons(buttons) 148 | self._device_buttons.set_buttons(buttons) 149 | self._menu.set_buttons(buttons) 150 | if buttons == None: self._modes.pop_mode('menu') 151 | self.update(); 152 | 153 | def update(self): 154 | super(DeviceComponentEx, self).update() 155 | self._check_device() 156 | 157 | def _toggle_param_offset(self): 158 | self._device.toggle_param_offset() 159 | self._update_menu_actions() 160 | 161 | def _update_menu_actions(self): 162 | pcolor = 'Device.NormalParams' if not self._device._param_offset else 'Device.OffsetParams' 163 | self._menu.update_action(1, (pcolor, self._toggle_param_offset, None)) 164 | 165 | def _lock_device(self, offset = False): 166 | focused = self.song().appointed_device 167 | self._device.set_param_offset(offset) 168 | self._device.set_lock_to_device(True, focused) 169 | self._modes.push_mode('device') 170 | self._update_menu_actions() 171 | self.update() 172 | 173 | def _unlock_device(self): 174 | self._device.set_lock_to_device(False, None) 175 | self._modes.pop_mode('device') 176 | self._modes.pop_mode('menu') 177 | self._device.set_param_offset(False) 178 | self.update() 179 | 180 | def _select_device(self): 181 | self.song().view.select_device(self._device._device) 182 | self.update() 183 | 184 | def _on_param(self, idx, value = True): 185 | snap_index = idx + (4 if self._device._param_offset else 0) 186 | mode = self._snap_modes[snap_index] 187 | if mode == SnapModes.HALF: 188 | self._on_param_half_snap(idx, value) 189 | elif mode == SnapModes.REVERSE_HALF: 190 | self._on_param_reverse_half_snap(idx, value) 191 | elif mode == SnapModes.FULL: 192 | self._on_param_full_snap(idx, value) 193 | 194 | def _on_param_reverse_half_snap(self, idx, value): 195 | """ Restore on rising edge, cache on falling edge """ 196 | param = self._device.get_parameter(idx) 197 | cached = self._param_values[idx] 198 | if value: 199 | if cached is not None: self._set_parameter_value(param, cached) 200 | else: 201 | self._param_values[idx] = param.value 202 | 203 | def _on_param_half_snap(self, idx, value): 204 | """ Cache on rising edge, restore on falling edge """ 205 | param = self._device.get_parameter(idx) 206 | cached = self._param_values[idx] 207 | if value: 208 | self._param_values[idx] = param.value 209 | else: 210 | if cached is not None: self._set_parameter_value(param, cached) 211 | 212 | def _on_param_full_snap(self, idx, value): 213 | """ Cache and restore on rising and falling edge""" 214 | param = self._device.get_parameter(idx) 215 | cached = self._param_values[idx] 216 | self._param_values[idx] = param.value 217 | if cached is not None: self._set_parameter_value(param, cached) 218 | 219 | def _on_toggle_snap_mode(self, idx): 220 | pass 221 | 222 | def _set_parameter_value(self, param, value): 223 | current = param.value 224 | def restore(): 225 | param.value = current 226 | param.value = value 227 | param.value = value 228 | self.schedule_message(1, restore) 229 | 230 | def _check_device(self): 231 | d = self._device._device 232 | if liveobj_valid(d): return 233 | self._device.set_lock_to_device(False, None) 234 | self._modes.pop_mode('menu') 235 | self._modes.pop_mode('device') 236 | 237 | --------------------------------------------------------------------------------