├── push2_python ├── simulator │ ├── __init__.py │ ├── simulator.py │ └── templates │ │ └── index.html ├── exceptions.py ├── touchstrip.py ├── classes.py ├── encoders.py ├── buttons.py ├── constants.py ├── display.py ├── pads.py ├── push2_map.py └── __init__.py ├── simulator.png ├── .gitignore ├── setup.py ├── LICENSE └── README.md /push2_python/simulator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simulator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffont/push2-python/HEAD/simulator.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | __pycache__/ 4 | .vscode/ 5 | test.py 6 | test_speed.py 7 | push_doc 8 | test_img_960x160.png -------------------------------------------------------------------------------- /push2_python/exceptions.py: -------------------------------------------------------------------------------- 1 | class Push2USBDeviceNotFound(Exception): 2 | pass 3 | 4 | class Push2USBDeviceConfigurationError(Exception): 5 | pass 6 | 7 | class Push2MIDIeviceNotFound(Exception): 8 | pass 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name='push2-python', 4 | version='0.6', 5 | description='Utils to interface with Ableton\'s Push 2 from Python', 6 | url='https://github.com/ffont/push2-python', 7 | author='Frederic Font', 8 | author_email='frederic.font@gmail.com', 9 | license='MIT', 10 | install_requires=['numpy', 'pyusb', 'python-rtmidi', 'mido', 'flask', 'flask-socketio', 'eventlet' , 'pillow'], 11 | python_requires='>=3', 12 | setup_requires=['setuptools_scm'], 13 | include_package_data=True, 14 | packages=find_packages() 15 | ) 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Frederic Font 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 | -------------------------------------------------------------------------------- /push2_python/touchstrip.py: -------------------------------------------------------------------------------- 1 | import mido 2 | import weakref 3 | from .constants import MIDO_PITCWHEEL, MIDO_CONTROLCHANGE, ACTION_TOUCHSTRIP_TOUCHED, PUSH2_SYSEX_PREFACE_BYTES, PUSH2_SYSEX_END_BYTES 4 | from .classes import AbstractPush2Section 5 | 6 | 7 | class Push2TouchStrip(AbstractPush2Section): 8 | """Class to interface with Ableton's Touch Strip. 9 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#Touch%20Strip 10 | """ 11 | 12 | def set_modulation_wheel_mode(self): 13 | """Configure touchstrip to act as a modulation wheel 14 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#2101-touch-strip-configuration 15 | """ 16 | msg = mido.Message.from_bytes(PUSH2_SYSEX_PREFACE_BYTES + [0x17, 0x0C] + PUSH2_SYSEX_END_BYTES) 17 | self.push.send_midi_to_push(msg) 18 | 19 | def set_pitch_bend_mode(self): 20 | """Configure touchstrip to act as a pitch bend wheel (this is the default) 21 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#2101-touch-strip-configuration 22 | """ 23 | msg = mido.Message.from_bytes(PUSH2_SYSEX_PREFACE_BYTES + [0x17, 0x68] + PUSH2_SYSEX_END_BYTES) 24 | self.push.send_midi_to_push(msg) 25 | 26 | def on_midi_message(self, message): 27 | if message.type == MIDO_PITCWHEEL: 28 | value = message.pitch 29 | self.push.trigger_action(ACTION_TOUCHSTRIP_TOUCHED, value) 30 | return True 31 | elif message.type == MIDO_CONTROLCHANGE: 32 | value = message.value 33 | self.push.trigger_action(ACTION_TOUCHSTRIP_TOUCHED, value) 34 | return True 35 | -------------------------------------------------------------------------------- /push2_python/classes.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | import functools 3 | import time 4 | 5 | 6 | def function_call_interval_limit(interval): 7 | """Decorator that makes sure the decorated function is only executed once in the given 8 | time interval (in seconds). It stores the last time the decorated function was executed 9 | and if it was less than "interval" seconds ago, a dummy function is returned instead. 10 | This decorator also check at runtime if the first argument of the decorated function call 11 | has the porperty "function_call_interval_limit_overwrite" exists. If that is the cases, 12 | it uses its value as interval rather than the "interval" value passed in the decorator 13 | definition. 14 | """ 15 | def decorator(func): 16 | @functools.wraps(func) 17 | def wrapper(*args, **kwargs): 18 | current_time = time.time() 19 | last_time_called_key = '_last_time_called_{0}'.format(func.__name__) 20 | if not hasattr(function_call_interval_limit, last_time_called_key): 21 | setattr(function_call_interval_limit, last_time_called_key, current_time) 22 | return func(*args, **kwargs) 23 | 24 | try: 25 | # First argument in the func call should be class instance (i.e. self), try to get interval 26 | # definition from calss at runtime so it is adjustable 27 | new_interval = args[0].function_call_interval_limit_overwrite 28 | interval = new_interval 29 | except AttributeError: 30 | # If property "function_call_interval_limit_overwrite" not found in class instance, just use the interval 31 | # given in the decorator definition 32 | pass 33 | 34 | if current_time - getattr(function_call_interval_limit, last_time_called_key) >= interval: 35 | setattr(function_call_interval_limit, last_time_called_key, current_time) 36 | return func(*args, **kwargs) 37 | else: 38 | return lambda *args: None 39 | 40 | return wrapper 41 | return decorator 42 | 43 | class AbstractPush2Section(object): 44 | """Abstract class to be inherited when implementing the interfacing with specific sections 45 | of Push2. It implements an init method which gets a reference to the main Push2 object and adds 46 | a property method to get it de-referenced. 47 | """ 48 | 49 | main_push2_object = None 50 | 51 | def __init__(self, main_push_object): 52 | self.main_push_object = weakref.ref(main_push_object) 53 | 54 | @property 55 | def push(self): 56 | return self.main_push_object() # Return de-refernced main Push2 object 57 | -------------------------------------------------------------------------------- /push2_python/encoders.py: -------------------------------------------------------------------------------- 1 | import mido 2 | from .constants import ANIMATION_DEFAULT, MIDO_CONTROLCHANGE, \ 3 | MIDO_NOTEON, MIDO_NOTEOFF, ACTION_ENCODER_ROTATED, ACTION_ENCODER_TOUCHED, ACTION_ENCODER_RELEASED 4 | from .classes import AbstractPush2Section 5 | 6 | 7 | def get_individual_encoder_action_name(action_name, encoder_name): 8 | return '{0} - {1}'.format(action_name, encoder_name) 9 | 10 | 11 | class Push2Encoders(AbstractPush2Section): 12 | """Class to interface with Ableton's Push2 encoders. 13 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#Encoders 14 | """ 15 | 16 | encoder_map = None 17 | encoder_touch_map = None 18 | encoder_names_index = None 19 | encoder_names_list = None 20 | 21 | def __init__(self, *args, **kwargs): 22 | super().__init__(*args, **kwargs) 23 | self.encoder_map = { 24 | data['Number']: data for data in self.push.push2_map['Parts']['RotaryEncoders']} 25 | self.encoder_touch_map = { 26 | data['Touch']['Number']: data for data in self.push.push2_map['Parts']['RotaryEncoders']} 27 | self.encoder_names_index = {data['Name']: data['Number'] 28 | for data in self.push.push2_map['Parts']['RotaryEncoders']} 29 | self.encoder_names_list = list(self.encoder_names_index.keys()) 30 | 31 | @property 32 | def available_names(self): 33 | return self.encoder_names_list 34 | 35 | def encoder_name_to_encoder_n(self, encoder_name): 36 | """ 37 | Gets encoder number from given encoder name 38 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#23-midi-mapping 39 | """ 40 | return self.encoder_names_index.get(encoder_name, None) 41 | 42 | def on_midi_message(self, message): 43 | if message.type == MIDO_CONTROLCHANGE: # Encoder rotated 44 | if message.control in self.encoder_map: # CC number corresponds to one of the encoders 45 | if message.type == MIDO_CONTROLCHANGE: 46 | encoder = self.encoder_map[message.control] 47 | action = ACTION_ENCODER_ROTATED 48 | value = message.value 49 | if message.value > 63: 50 | # Counter-clockwise movement, see https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#Encoders 51 | value = -1 * (128 - message.value) 52 | self.push.trigger_action(action, encoder['Name'], value) # Trigger generic rotate encoder action 53 | self.push.trigger_action(get_individual_encoder_action_name( 54 | action, encoder['Name']), value) # Trigger individual rotate encoder action as well 55 | return True 56 | elif message.type in [MIDO_NOTEON, MIDO_NOTEOFF]: # Encoder touched or released 57 | if message.note in self.encoder_touch_map: # Note number corresponds to one of the encoders in touch mode 58 | encoder = self.encoder_touch_map[message.note] 59 | action = ACTION_ENCODER_TOUCHED if message.velocity == 127 else ACTION_ENCODER_RELEASED 60 | self.push.trigger_action(action, encoder['Name']) # Trigger generic touch/release encoder action 61 | self.push.trigger_action(get_individual_encoder_action_name( 62 | action, encoder['Name'])) # Trigger individual touch/release encoder action as well 63 | return True 64 | -------------------------------------------------------------------------------- /push2_python/buttons.py: -------------------------------------------------------------------------------- 1 | import mido 2 | from .constants import ANIMATION_DEFAULT, MIDO_CONTROLCHANGE, ACTION_BUTTON_PRESSED, ACTION_BUTTON_RELEASED, ANIMATION_STATIC 3 | from .classes import AbstractPush2Section 4 | 5 | 6 | def get_individual_button_action_name(action_name, button_name): 7 | return '{0} - {1}'.format(action_name, button_name) 8 | 9 | 10 | class Push2Buttons(AbstractPush2Section): 11 | """Class to interface with Ableton's Push2 buttons. 12 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#Buttons 13 | """ 14 | 15 | button_map = None 16 | button_names_index = None 17 | button_names_list = None 18 | 19 | def __init__(self, *args, **kwargs): 20 | super().__init__(*args, **kwargs) 21 | self.button_map = {data['Number']: data for data in self.push.push2_map['Parts']['Buttons']} 22 | self.button_names_index = {data['Name']: data['Number'] for data in self.push.push2_map['Parts']['Buttons']} 23 | self.button_names_list = list(self.button_names_index.keys()) 24 | 25 | @property 26 | def available_names(self): 27 | return self.button_names_list 28 | 29 | def button_name_to_button_n(self, button_name): 30 | """ 31 | Gets button number from given button name 32 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#23-midi-mapping 33 | """ 34 | return self.button_names_index.get(button_name, None) 35 | 36 | def set_button_color(self, button_name, color='white', animation=ANIMATION_DEFAULT, animation_end_color='black'): 37 | """Sets the color of the button with given name. 38 | 'color' must be a valid RGB or BW color name present in the color palette. See push2_python.constants.DEFAULT_COLOR_PALETTE for default color names. 39 | If the button only acceps BW colors, the color name will be matched against the BW palette, otherwise it will be matched against RGB palette. 40 | 'animation' must be a valid animation name from those defined in push2_python.contants.ANIMATION_*. Note that to configure an animation, both 41 | the 'start' and 'end' colors of the animation need to be defined. The 'start' color is defined by 'color' parameter. The 'end' color is defined 42 | by the color specified in 'animation_end_color', which must be a valid RGB color name present in the color palette. 43 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#setting-led-colors 44 | """ 45 | button_n = self.button_name_to_button_n(button_name) 46 | if button_n is not None: 47 | button = self.button_map[button_n] 48 | if button['Color']: 49 | color_idx = self.push.get_rgb_color(color) 50 | black_color_idx = self.push.get_rgb_color(animation_end_color) 51 | else: 52 | color_idx = self.push.get_bw_color(color) 53 | black_color_idx = self.push.get_bw_color(animation_end_color) 54 | if animation != ANIMATION_STATIC: 55 | # If animation is not static, we first set the button to black color with static animation so then, when setting 56 | # the desired color with the corresponding animation it lights as expected. 57 | # This behaviour should be furhter investigated as this could maybe be optimized. 58 | msg = mido.Message(MIDO_CONTROLCHANGE, control=button_n, value=black_color_idx, channel=ANIMATION_STATIC) 59 | self.push.send_midi_to_push(msg) 60 | msg = mido.Message(MIDO_CONTROLCHANGE, control=button_n, value=color_idx, channel=animation) 61 | self.push.send_midi_to_push(msg) 62 | 63 | if self.push.simulator_controller is not None: 64 | self.push.simulator_controller.set_element_color('cc' + str(button_n), color_idx, animation) 65 | 66 | def set_all_buttons_color(self, color='white', animation=ANIMATION_DEFAULT, animation_end_color='black'): 67 | """Sets the color of all buttons in Push2 to the given color. 68 | 'color' must be a valid RGB or BW color name present in the color palette. See push2_python.constants.DEFAULT_COLOR_PALETTE for default color names. 69 | If the button only acceps BW colors, the color name will be matched against the BW palette, otherwise it will be matched against RGB palette. 70 | 'animation' must be a valid animation name from those defined in push2_python.contants.ANIMATION_*. Note that to configure an animation, both 71 | the 'start' and 'end' colors of the animation need to be defined. The 'start' color is defined by 'color' parameter. The 'end' color is defined 72 | by the color specified in 'animation_end_color', which must be a valid RGB color name present in the color palette. 73 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#setting-led-colors 74 | """ 75 | for button_name in self.available_names: 76 | self.set_button_color(button_name, color=color, animation=animation, animation_end_color=animation_end_color) 77 | 78 | def on_midi_message(self, message): 79 | if message.type == MIDO_CONTROLCHANGE: 80 | if message.control in self.button_map: # CC number corresponds to one of the buttons 81 | button = self.button_map[message.control] 82 | action = ACTION_BUTTON_PRESSED if message.value == 127 else ACTION_BUTTON_RELEASED 83 | self.push.trigger_action(action, button['Name']) # Trigger generic button action 84 | self.push.trigger_action(get_individual_button_action_name(action, button['Name'])) # Trigger individual button action as well 85 | return True 86 | 87 | -------------------------------------------------------------------------------- /push2_python/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | 4 | # Push2 map file 5 | PUSH2_MAP_FILE_PATH = os.path.join(os.path.dirname(__file__), 'Push2-map.json') 6 | 7 | # USB device/transfer settings 8 | ABLETON_VENDOR_ID = 0x2982 9 | PUSH2_PRODUCT_ID = 0x1967 10 | USB_TRANSFER_TIMEOUT = 1000 11 | 12 | # MIDI PORT NAMES 13 | 14 | def is_push_midi_in_port_name(port_name, use_user_port=False): 15 | """Returns True if the given 'port_name' is the MIDI port name corresponding to Push2 MIDI 16 | input for the current OS platform. If 'use_user_port', it will check against Push2 User port instead 17 | of Push2 Live port. 18 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#21-midi-interface-access 19 | """ 20 | if platform.system() == "Linux": 21 | if not use_user_port: 22 | return 'Ableton Push' in port_name and port_name.endswith(':0') # 'Ableton Push 2 nn:0', with nn being a variable number 23 | else: 24 | return 'Ableton Push' in port_name and port_name.endswith(':1') # 'Ableton Push 2 nn:1', with nn being a variable number 25 | elif platform.system() == "Windows": 26 | if not use_user_port: # this uses the Ableton Live Midi Port 27 | return 'Ableton Push 2' in port_name # 'Ableton Push 2 nn', with nn being a variable number 28 | else: # user port 29 | return 'MIDIIN2 (Ableton Push 2)' in port_name # 'MIDIIN2 (Ableton Push 2) nn', with nn being a variable number 30 | else: #macOS 31 | if not use_user_port: 32 | return 'Ableton Push 2 Live Port' in port_name 33 | else: 34 | return 'Ableton Push 2 User Port' in port_name 35 | 36 | 37 | def is_push_midi_out_port_name(port_name, use_user_port=False): 38 | """Returns True if the given 'port_name' is the MIDI port name corresponding to Push2 MIDI 39 | output for the current OS platform. If 'use_user_port', it will check against Push2 User port instead 40 | of Push2 Live port. 41 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#21-midi-interface-access 42 | """ 43 | if platform.system() == "Linux": 44 | if not use_user_port: 45 | return 'Ableton Push' in port_name and port_name.endswith(':0') # 'Ableton Push 2 nn:0', with nn being a variable number 46 | else: 47 | return 'Ableton Push' in port_name and port_name.endswith(':1') # 'Ableton Push 2 nn:1', with nn being a variable number 48 | elif platform.system() == "Windows": 49 | if not use_user_port: # ableton live midi port 50 | return 'Ableton Push 2' in port_name # 'Ableton Push 2 nn', with nn being a variable number 51 | else: # user port 52 | return 'MIDIOUT2 (Ableton Push 2)' in port_name # 'MIDIIN2 (Ableton Push 2) nn', with nn being a variable number 53 | else: #macOS 54 | if not use_user_port: 55 | return 'Ableton Push 2 Live Port' == port_name 56 | else: 57 | return 'Ableton Push 2 User Port' == port_name 58 | 59 | 60 | PUSH2_RECONNECT_INTERVAL = 0.05 # 50 ms 61 | PUSH2_MIDI_ACTIVE_SENSING_MAX_INTERVAL = 0.5 # 0.5 seconds 62 | 63 | MIDO_NOTEON = 'note_on' 64 | MIDO_NOTEOFF = 'note_off' 65 | MIDO_POLYAT = 'polytouch' 66 | MIDO_AFTERTOUCH = 'aftertouch' 67 | MIDO_PITCWHEEL = 'pitchwheel' 68 | MIDO_CONTROLCHANGE = 'control_change' 69 | 70 | PUSH2_SYSEX_PREFACE_BYTES = [0xF0, 0x00, 0x21, 0x1D, 0x01, 0x01] 71 | PUSH2_SYSEX_END_BYTES = [0xF7] 72 | 73 | # Push 2 Display 74 | DISPLAY_FRAME_HEADER = [0xff, 0xcc, 0xaa, 0x88, 75 | 0x00, 0x00, 0x00, 0x00, 76 | 0x00, 0x00, 0x00, 0x00, 77 | 0x00, 0x00, 0x00, 0x00] 78 | DISPLAY_N_LINES = 160 79 | DISPLAY_LINE_PIXELS = 960 80 | DISPLAY_PIXEL_BYTES = 2 # bytes 81 | DISPLAY_LINE_FILLER_BYTES = 128 82 | DISPLAY_LINE_SIZE = DISPLAY_LINE_PIXELS * \ 83 | DISPLAY_PIXEL_BYTES + DISPLAY_LINE_FILLER_BYTES 84 | DISPLAY_N_LINES_PER_BUFFER = 8 85 | DISPLAY_BUFFER_SIZE = DISPLAY_LINE_SIZE * DISPLAY_N_LINES_PER_BUFFER 86 | DISPLAY_FRAME_XOR_PATTERN = [0xE7F3, 0xE7FF] * ( 87 | ((DISPLAY_LINE_PIXELS + (DISPLAY_LINE_FILLER_BYTES // 2)) * DISPLAY_N_LINES) // 2) 88 | FRAME_FORMAT_BGR565 = 'bgr565' 89 | FRAME_FORMAT_RGB565 = 'rgb565' 90 | FRAME_FORMAT_RGB = 'rgb' 91 | 92 | # LED rgb default color palette 93 | # Color palette is defined as a dictionary where keys are a color index [0..127] and 94 | # values are a 2-element list with the first element corresponding to the given RGB color name 95 | # for that index and the second element being the given BW color name for that index 96 | # This palette can be cusomized using `Push2.set_color_palette_entry` method. 97 | DEFAULT_COLOR_PALETTE = { 98 | 0: ['black', 'black'], 99 | 3: ['orange', None], 100 | 8: ['yellow', None], 101 | 15: ['turquoise', None], 102 | 16: [None, 'dark_gray'], 103 | 22: ['purple', None], 104 | 25: ['pink', None], 105 | 48: [None, 'light_gray'], 106 | 122: ['white', None], 107 | 123: ['light_gray', None], 108 | 124: ['dark_gray', None], 109 | 125: ['blue', None], 110 | 126: ['green', None], 111 | 127: ['red', 'white'] 112 | } 113 | DEFAULT_RGB_COLOR = 126 114 | DEFAULT_BW_COLOR = 127 115 | 116 | # Led animations 117 | # Because push2-python does not send MIDI clock messages to push, all animations will run synced to a 120bpm tempo 118 | # See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#268-led-animation 119 | # for more info on what animation names mean 120 | ANIMATION_STATIC = 0 121 | ANIMATION_ONESHOT_24TH = 1 122 | ANIMATION_ONESHOT_16TH = 2 123 | ANIMATION_ONESHOT_8TH = 3 124 | ANIMATION_ONESHOT_QUARTER = 4 125 | ANIMATION_ONESHOT_HALF = 5 126 | ANIMATION_PULSING_24TH = 6 127 | ANIMATION_PULSING_16TH = 7 128 | ANIMATION_PULSING_8TH = 8 129 | ANIMATION_PULSING_QUARTER = 9 130 | ANIMATION_PULSING_HALF = 10 131 | ANIMATION_BLINKING_24TH = 11 132 | ANIMATION_BLINKING_16TH = 12 133 | ANIMATION_BLINKING_8TH = 13 134 | ANIMATION_BLINKING_QUARTER = 14 135 | ANIMATION_BLINKING_HALF = 15 136 | ANIMATION_DEFAULT = ANIMATION_STATIC 137 | 138 | # Push2 actions 139 | ACTION_PAD_PRESSED = 'on_pad_pressed' 140 | ACTION_PAD_RELEASED = 'on_pad_released' 141 | ACTION_PAD_AFTERTOUCH = 'on_pad_aftertouch' 142 | ACTION_TOUCHSTRIP_TOUCHED = 'on_touchstrip_touched' 143 | ACTION_BUTTON_PRESSED = 'on_button_pressed' 144 | ACTION_BUTTON_RELEASED = 'on_button_released' 145 | ACTION_ENCODER_ROTATED = 'on_encoder_rotated' 146 | ACTION_ENCODER_TOUCHED = 'on_encoder_touched' 147 | ACTION_ENCODER_RELEASED = 'on_encoder_released' 148 | ACTION_DISPLAY_CONNECTED = 'on_display_connected' 149 | ACTION_DISPLAY_DISCONNECTED = 'on_display_disconnected' 150 | ACTION_MIDI_CONNECTED = 'on_midi_connected' 151 | ACTION_MIDI_DISCONNECTED = 'on_midi_disconnected' 152 | ACTION_SUSTAIN_PEDAL = 'on_sustain_pedal' 153 | 154 | # Push2 button names 155 | # NOTE: the list of button names is here to facilitate autocompletion when developing apps using push2_python package, but is not needed for the package 156 | # This list was generated using the following code: 157 | # import json 158 | # data = json.load(open('push2_python/Push2-map.json')) 159 | # for item in data['Parts']['Buttons']: 160 | # print('BUTTON_{0} = \'{1}\''.format(item['Name'].replace(' ', '_').replace('/', '_').upper(), item['Name'])) 161 | BUTTON_TAP_TEMPO = 'Tap Tempo' 162 | BUTTON_METRONOME = 'Metronome' 163 | BUTTON_DELETE = 'Delete' 164 | BUTTON_UNDO = 'Undo' 165 | BUTTON_MUTE = 'Mute' 166 | BUTTON_SOLO = 'Solo' 167 | BUTTON_STOP = 'Stop' 168 | BUTTON_CONVERT = 'Convert' 169 | BUTTON_DOUBLE_LOOP = 'Double Loop' 170 | BUTTON_QUANTIZE = 'Quantize' 171 | BUTTON_DUPLICATE = 'Duplicate' 172 | BUTTON_NEW = 'New' 173 | BUTTON_FIXED_LENGTH = 'Fixed Length' 174 | BUTTON_AUTOMATE = 'Automate' 175 | BUTTON_RECORD = 'Record' 176 | BUTTON_PLAY = 'Play' 177 | BUTTON_UPPER_ROW_1 = 'Upper Row 1' 178 | BUTTON_UPPER_ROW_2 = 'Upper Row 2' 179 | BUTTON_UPPER_ROW_3 = 'Upper Row 3' 180 | BUTTON_UPPER_ROW_4 = 'Upper Row 4' 181 | BUTTON_UPPER_ROW_5 = 'Upper Row 5' 182 | BUTTON_UPPER_ROW_6 = 'Upper Row 6' 183 | BUTTON_UPPER_ROW_7 = 'Upper Row 7' 184 | BUTTON_UPPER_ROW_8 = 'Upper Row 8' 185 | BUTTON_LOWER_ROW_1 = 'Lower Row 1' 186 | BUTTON_LOWER_ROW_2 = 'Lower Row 2' 187 | BUTTON_LOWER_ROW_3 = 'Lower Row 3' 188 | BUTTON_LOWER_ROW_4 = 'Lower Row 4' 189 | BUTTON_LOWER_ROW_5 = 'Lower Row 5' 190 | BUTTON_LOWER_ROW_6 = 'Lower Row 6' 191 | BUTTON_LOWER_ROW_7 = 'Lower Row 7' 192 | BUTTON_LOWER_ROW_8 = 'Lower Row 8' 193 | BUTTON_1_32T = '1/32t' 194 | BUTTON_1_32 = '1/32' 195 | BUTTON_1_16T = '1/16t' 196 | BUTTON_1_16 = '1/16' 197 | BUTTON_1_8T = '1/8t' 198 | BUTTON_1_8 = '1/8' 199 | BUTTON_1_4T = '1/4t' 200 | BUTTON_1_4 = '1/4' 201 | BUTTON_SETUP = 'Setup' 202 | BUTTON_USER = 'User' 203 | BUTTON_ADD_DEVICE = 'Add Device' 204 | BUTTON_ADD_TRACK = 'Add Track' 205 | BUTTON_DEVICE = 'Device' 206 | BUTTON_MIX = 'Mix' 207 | BUTTON_BROWSE = 'Browse' 208 | BUTTON_CLIP = 'Clip' 209 | BUTTON_MASTER = 'Master' 210 | BUTTON_UP = 'Up' 211 | BUTTON_DOWN = 'Down' 212 | BUTTON_LEFT = 'Left' 213 | BUTTON_RIGHT = 'Right' 214 | BUTTON_REPEAT = 'Repeat' 215 | BUTTON_ACCENT = 'Accent' 216 | BUTTON_SCALE = 'Scale' 217 | BUTTON_LAYOUT = 'Layout' 218 | BUTTON_NOTE = 'Note' 219 | BUTTON_SESSION = 'Session' 220 | BUTTON_OCTAVE_UP = 'Octave Up' 221 | BUTTON_OCTAVE_DOWN = 'Octave Down' 222 | BUTTON_PAGE_LEFT = 'Page Left' 223 | BUTTON_PAGE_RIGHT = 'Page Right' 224 | BUTTON_SHIFT = 'Shift' 225 | BUTTON_SELECT = 'Select' 226 | 227 | # Push2 encoder names 228 | # NOTE: the list of encoder names is here to facilitate autocompletion when developing apps using push2_python package, but is not needed for the package 229 | # This list was generated using the following code: 230 | # import json 231 | # data = json.load(open('push2_python/Push2-map.json')) 232 | # for item in data['Parts']['RotaryEncoders']: 233 | # print('ENCODER_{0} = \'{1}\''.format(item['Name'].replace(' ', '_').upper(), item['Name'])) 234 | ENCODER_TEMPO_ENCODER = 'Tempo Encoder' # Left-most encoder 235 | ENCODER_SWING_ENCODER = 'Swing Encoder' 236 | ENCODER_TRACK1_ENCODER = 'Track1 Encoder' 237 | ENCODER_TRACK2_ENCODER = 'Track2 Encoder' 238 | ENCODER_TRACK3_ENCODER = 'Track3 Encoder' 239 | ENCODER_TRACK4_ENCODER = 'Track4 Encoder' 240 | ENCODER_TRACK5_ENCODER = 'Track5 Encoder' 241 | ENCODER_TRACK6_ENCODER = 'Track6 Encoder' 242 | ENCODER_TRACK7_ENCODER = 'Track7 Encoder' 243 | ENCODER_TRACK8_ENCODER = 'Track8 Encoder' 244 | ENCODER_MASTER_ENCODER = 'Master Encoder' # Right-most encoder 245 | -------------------------------------------------------------------------------- /push2_python/display.py: -------------------------------------------------------------------------------- 1 | import usb.core 2 | import usb.util 3 | import numpy 4 | import logging 5 | import time 6 | from .classes import AbstractPush2Section, function_call_interval_limit 7 | from .exceptions import Push2USBDeviceConfigurationError, Push2USBDeviceNotFound 8 | from .constants import ABLETON_VENDOR_ID, PUSH2_PRODUCT_ID, USB_TRANSFER_TIMEOUT, DISPLAY_FRAME_HEADER, \ 9 | DISPLAY_BUFFER_SIZE, DISPLAY_FRAME_XOR_PATTERN, DISPLAY_N_LINES, DISPLAY_LINE_PIXELS, DISPLAY_LINE_FILLER_BYTES, \ 10 | FRAME_FORMAT_BGR565, FRAME_FORMAT_RGB565, FRAME_FORMAT_RGB, PUSH2_RECONNECT_INTERVAL, ACTION_DISPLAY_CONNECTED, \ 11 | ACTION_DISPLAY_DISCONNECTED 12 | 13 | NP_DISPLAY_FRAME_XOR_PATTERN = numpy.array(DISPLAY_FRAME_XOR_PATTERN, dtype=numpy.uint16) # Numpy array version of the constant 14 | 15 | 16 | def rgb565_to_bgr565(rgb565_frame): 17 | r_filter = int('1111100000000000', 2) 18 | g_filter = int('0000011111100000', 2) 19 | b_filter = int('0000000000011111', 2) 20 | frame_r_filtered = numpy.bitwise_and(rgb565_frame, r_filter) 21 | frame_r_shifted = numpy.right_shift(frame_r_filtered, 11) # Shift bits so R compoenent goes to the right 22 | frame_g_filtered = numpy.bitwise_and(rgb565_frame, g_filter) 23 | frame_g_shifted = frame_g_filtered # No need to shift green, it stays in the same position 24 | frame_b_filtered = numpy.bitwise_and(rgb565_frame, b_filter) 25 | frame_b_shifted = numpy.left_shift(frame_b_filtered, 11) # Shift bits so B compoenent goes to the left 26 | return frame_r_shifted + frame_g_shifted + frame_b_shifted # Combine all channels 27 | 28 | 29 | # Non-vectorized function for converting from rgb to bgr565 30 | def rgb_to_bgr565(rgb_frame): 31 | rgb_frame *= 255 32 | rgb_frame_r = rgb_frame[:, :, 0].astype(numpy.uint16) 33 | rgb_frame_g = rgb_frame[:, :, 1].astype(numpy.uint16) 34 | rgb_frame_b = rgb_frame[:, :, 2].astype(numpy.uint16) 35 | frame_r_filtered = numpy.bitwise_and(rgb_frame_r, int('0000000011111000', 2)) 36 | frame_r_shifted = numpy.right_shift(frame_r_filtered, 3) 37 | frame_g_filtered = numpy.bitwise_and(rgb_frame_g, int('0000000011111100', 2)) 38 | frame_g_shifted = numpy.left_shift(frame_g_filtered, 3) 39 | frame_b_filtered = numpy.bitwise_and(rgb_frame_b, int('0000000011111000', 2)) 40 | frame_b_shifted = numpy.left_shift(frame_b_filtered, 8) 41 | combined = frame_r_shifted + frame_g_shifted + frame_b_shifted # Combine all channels 42 | return combined.transpose() 43 | 44 | class Push2Display(AbstractPush2Section): 45 | """Class to interface with Ableton's Push2 display. 46 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#display-interface 47 | """ 48 | usb_endpoint = None 49 | last_prepared_frame = None 50 | function_call_interval_limit_overwrite = PUSH2_RECONNECT_INTERVAL 51 | 52 | 53 | @function_call_interval_limit(PUSH2_RECONNECT_INTERVAL) 54 | def configure_usb_device(self): 55 | """Connect to Push2 USB device and get the Endpoint object used to send data 56 | to Push2's display. 57 | 58 | This function is decorated with 'function_call_interval_limit' which means that it is only going to be executed if 59 | PUSH2_RECONNECT_INTERVAL seconds have passed since the last time the function was called. This is to avoid potential 60 | problems trying to configure display many times per second. 61 | 62 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#31-usb-display-interface-access 63 | """ 64 | usb_device = None 65 | try: 66 | usb_device = usb.core.find( 67 | idVendor=ABLETON_VENDOR_ID, idProduct=PUSH2_PRODUCT_ID) 68 | except usb.core.NoBackendError: 69 | logging.error('No backend is available for pyusb. Please make sure \'libusb\' is installed in your system.') 70 | 71 | if usb_device is None: 72 | raise Push2USBDeviceNotFound 73 | 74 | device_configuration = usb_device.get_active_configuration() 75 | if device_configuration is None: 76 | usb_device.set_configuration() 77 | 78 | interface = device_configuration[(0, 0)] 79 | out_endpoint = usb.util.find_descriptor( 80 | interface, 81 | custom_match=lambda e: 82 | usb.util.endpoint_direction(e.bEndpointAddress) == 83 | usb.util.ENDPOINT_OUT) 84 | 85 | if out_endpoint is None: 86 | raise Push2USBDeviceConfigurationError 87 | 88 | try: 89 | # Try sending a framr header as a test... 90 | out_endpoint.write(DISPLAY_FRAME_HEADER, USB_TRANSFER_TIMEOUT) 91 | black_frame = self.prepare_frame(self.make_black_frame(), input_format=FRAME_FORMAT_BGR565) 92 | out_endpoint.write(black_frame, USB_TRANSFER_TIMEOUT) 93 | except usb.core.USBError: 94 | self.usb_endpoint = None 95 | return 96 | 97 | # ...if it works (no USBError exception) set self.usb_endpoint and trigger action 98 | self.usb_endpoint = out_endpoint 99 | self.push.trigger_action(ACTION_DISPLAY_CONNECTED) 100 | 101 | 102 | def prepare_frame(self, frame, input_format=FRAME_FORMAT_BGR565): 103 | """Prepare the given image frame to be shown in the Push2's display. 104 | Depending on the input_format argument, "frame" must be a numpy array with the following characteristics: 105 | 106 | * for FRAME_FORMAT_BGR565: numpy array of shape 910x160 and of uint16. Each uint16 element specifies rgb 107 | color with the following bit position meaning: [b4 b3 b2 b1 b0 g5 g4 g3 g2 g1 g0 r4 r3 r2 r1 r0]. 108 | 109 | * for FRAME_FORMAT_RGB565: numpy array of shape 910x160 and of uint16. Each uint16 element specifies rgb 110 | color with the following bit position meaning: [r4 r3 r2 r1 r0 g5 g4 g3 g2 g1 g0 b4 b3 b2 b1 b0]. 111 | 112 | * for FRAME_FORMAT_RGB: numpy array of shape 910x160x3 with the third dimension representing rgb colors 113 | with separate float values for rgb channels (float values in range [0.0, 1.0]). 114 | 115 | Preferred format is brg565 as it requires no conversion before sending to Push2. Using brg565 is also very fast 116 | as color conversion is required but numpy handles it pretty well. You should be able to get frame rates higher than 117 | 30 fps, depending on the speed of your computer. However, using the rgb format (FRAME_FORMAT_RGB) will result in very 118 | long frame preparation times that can take seconds. This can be highgly optimized so it is as fast as the other formats 119 | but currently the library does not handle this format as nively. All numpy array elements are expected to be big endian. 120 | In addition to format conversion (if needed), "prepare_frame" prepares the frame to be sent to push by adding 121 | filler bytes and performing bitwise XOR as decribed in the Push2 specification. 122 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#326-allocating-libusb-transfers 123 | """ 124 | 125 | assert input_format in [FRAME_FORMAT_BGR565, FRAME_FORMAT_RGB565, FRAME_FORMAT_RGB], 'Invalid frame format' 126 | 127 | if input_format == FRAME_FORMAT_RGB: 128 | # If format is rgb, do conversion before the rest as frame must be reshaped 129 | # from (w, h, 3) to (w, h) 130 | frame = rgb_to_bgr565(frame) 131 | 132 | assert type(frame) == numpy.ndarray 133 | assert frame.dtype == numpy.dtype('uint16') 134 | assert frame.shape[0] == DISPLAY_LINE_PIXELS, 'Wrong number of pixels in line ({0})'.format( 135 | frame.shape[0]) 136 | assert frame.shape[1] == DISPLAY_N_LINES, 'Wrong number of lines in frame ({0})'.format( 137 | frame.shape[1]) 138 | 139 | width = DISPLAY_LINE_PIXELS + DISPLAY_LINE_FILLER_BYTES // 2 140 | height = DISPLAY_N_LINES 141 | prepared_frame = numpy.zeros(shape=(width, height), dtype=numpy.uint16) 142 | prepared_frame[0:frame.shape[0], 0:frame.shape[1]] = frame 143 | prepared_frame = prepared_frame.transpose().flatten() 144 | if input_format == FRAME_FORMAT_RGB565: 145 | prepared_frame = rgb565_to_bgr565(prepared_frame) 146 | elif input_format == FRAME_FORMAT_BGR565: 147 | pass # Nothing to do as this is already the requested format 148 | elif input_format == FRAME_FORMAT_RGB: 149 | pass # Nothing as conversion was done before 150 | prepared_frame = prepared_frame.byteswap() # Change to little endian 151 | prepared_frame = numpy.bitwise_xor(prepared_frame, NP_DISPLAY_FRAME_XOR_PATTERN) 152 | 153 | self.last_prepared_frame = prepared_frame 154 | return prepared_frame.byteswap().tobytes() 155 | 156 | 157 | def make_black_frame(self): 158 | return numpy.zeros((DISPLAY_LINE_PIXELS, DISPLAY_N_LINES), dtype=numpy.uint16) 159 | 160 | 161 | def send_to_display(self, prepared_frame): 162 | """Sends a prepared frame to Push2 display. 163 | First sends frame header and then sends prepared_frame in buffers of BUFFER_SIZE. 164 | 'prepared_frame' must be a flattened array of (DISPLAY_LINE_PIXELS + (DISPLAY_LINE_FILLER_BYTES // 2)) * DISPLAY_N_LINES 16bit BGR 565 values 165 | as returned by the 'Push2Display.prepare_frame' method. 166 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#326-allocating-libusb-transfers 167 | """ 168 | 169 | if self.usb_endpoint is None: 170 | try: 171 | self.configure_usb_device() 172 | except (Push2USBDeviceNotFound, Push2USBDeviceConfigurationError) as e: 173 | log_error = False 174 | if self.push.simulator_controller is not None: 175 | if not hasattr(self, 'display_init_error_shown'): 176 | log_error = True 177 | self.display_init_error_shown = True 178 | else: 179 | log_error = True 180 | if log_error: 181 | logging.error('Could not initialize Push 2 Display: {0}'.format(e)) 182 | 183 | if self.usb_endpoint is not None: 184 | try: 185 | self.usb_endpoint.write( 186 | DISPLAY_FRAME_HEADER, USB_TRANSFER_TIMEOUT) 187 | 188 | self.usb_endpoint.write(prepared_frame, USB_TRANSFER_TIMEOUT) 189 | 190 | # NOTE: code below was commented because the frames were apparently being 191 | # sent twice!! (nice bug...). There seems to be no need to send frame in chunks... 192 | #for i in range(0, len(prepared_frame), DISPLAY_BUFFER_SIZE): 193 | # buffer_data = prepared_frame[i: i + DISPLAY_BUFFER_SIZE] 194 | # self.usb_endpoint.write(buffer_data, USB_TRANSFER_TIMEOUT) 195 | 196 | except usb.core.USBError: 197 | # USB connection error, disable connection, will try to reconnect next time a frame is sent 198 | self.usb_endpoint = None 199 | self.push.trigger_action(ACTION_DISPLAY_DISCONNECTED) 200 | 201 | 202 | def display_frame(self, frame, input_format=FRAME_FORMAT_BGR565): 203 | prepared_frame = self.prepare_frame(frame.copy(), input_format=input_format) 204 | self.send_to_display(prepared_frame) 205 | 206 | if self.push.simulator_controller is not None: 207 | self.push.simulator_controller.prepare_and_display_in_simulator(frame.copy(), input_format=input_format) 208 | 209 | def display_last_frame(self): 210 | self.send_to_display(self.last_prepared_frame) 211 | -------------------------------------------------------------------------------- /push2_python/simulator/simulator.py: -------------------------------------------------------------------------------- 1 | import push2_python 2 | from flask import Flask, render_template 3 | from flask_socketio import SocketIO, emit 4 | from threading import Thread 5 | import threading 6 | import mido 7 | import base64 8 | from PIL import Image 9 | from io import BytesIO 10 | import time 11 | import numpy 12 | import queue 13 | import logging 14 | import mido 15 | 16 | app = Flask(__name__) 17 | sim_app = SocketIO(app) 18 | 19 | app_thread_id = None 20 | 21 | push_object = None 22 | client_connected = False 23 | 24 | midi_out = None 25 | 26 | 27 | default_color_palette = { 28 | 0: [(0, 0, 0), (0 ,0 ,0)], 29 | 3: [(265, 165, 0), None], 30 | 8: [(255, 255, 0), None], 31 | 15: [(0, 255, 255), None], 32 | 16: [None, (128, 128, 128)], 33 | 22: [(128, 0, 128), None], 34 | 25: [(255, 0, 255), None], 35 | 48: [None, (192, 192, 192)], 36 | 122: [(255, 255, 255), None], 37 | 123: [(192, 192, 192), None], 38 | 124: [(128, 128, 128), None], 39 | 125: [(0, 0, 255), None], 40 | 126: [(0, 255, 0), None], 41 | 127: [(255, 0, 0), (255, 255, 255)] 42 | } 43 | 44 | def make_midi_message_from_midi_trigger(midi_trigger, releasing=False, velocity=127, value=127): 45 | if midi_trigger.startswith('nn'): 46 | return mido.Message('note_on' if not releasing else 'note_off', note=int(midi_trigger.replace('nn', '')), velocity=velocity, channel=0) 47 | elif midi_trigger.startswith('cc'): 48 | return mido.Message('control_change', control=int(midi_trigger.replace('cc', '')), value=value if not releasing else 0, channel=0) 49 | return None 50 | 51 | 52 | class SimulatorController(object): 53 | 54 | next_frame = None 55 | black_frame = None 56 | last_time_frame_prepared = 0 57 | max_seconds_display_inactive = 2 58 | color_palette = default_color_palette 59 | ws_message_queue = queue.Queue() 60 | 61 | def __init__(self): 62 | 63 | # Generate black frame to be used if display is not updated 64 | colors = ['{b:05b}{g:06b}{r:05b}'.format(r=0, g=0, b=0), 65 | '{b:05b}{g:06b}{r:05b}'.format(r=0, g=0, b=0), 66 | '{b:05b}{g:06b}{r:05b}'.format(r=0, g=0, b=0)] 67 | colors = [int(c, 2) for c in colors] 68 | line_bytes = [] 69 | for i in range(0, 960): # 960 pixels per line 70 | if i <= 960 // 3: 71 | line_bytes.append(colors[0]) 72 | elif 960 // 3 < i <= 2 * 960 // 3: 73 | line_bytes.append(colors[1]) 74 | else: 75 | line_bytes.append(colors[2]) 76 | frame = [] 77 | for i in range(0, 160): # 160 lines 78 | frame.append(line_bytes) 79 | self.black_frame = numpy.array(frame, dtype=numpy.uint16).transpose() 80 | 81 | 82 | def emit_ws_message(self, name, data): 83 | if threading.get_ident() != app_thread_id: 84 | # The flask-socketio default configuration for web sockets does not support emitting to the browser from different 85 | # threads. It looks like this should be possible using some external queue based on redis or the like, but to avoid 86 | # further complicating the setup and requirements, if for some reason we're trying to emit tot he browser from a 87 | # different thread than the thread running the Flask server, we add the messages to a queue. That queue is being 88 | # continuously pulled (every 100ms) from the browser (see index.html) and then messages are sent. This means that 89 | # the timing of the messages won't be accurate, but this seems like a reaosnable trade-off considering the nature 90 | # and purpose of the simulator. 91 | self.ws_message_queue.put((name, data)) 92 | else: 93 | emit(name, data) 94 | 95 | def emit_messages_from_ws_queue(self): 96 | while not self.ws_message_queue.empty(): 97 | name, data = self.ws_message_queue.get() 98 | emit(name, data) 99 | 100 | if time.time() - self.last_time_frame_prepared > self.max_seconds_display_inactive: 101 | self.prepare_and_display_in_simulator(self.black_frame, input_format=push2_python.constants.FRAME_FORMAT_BGR565, force=True) 102 | 103 | def clear_color_palette(self): 104 | self.color_palette = {} 105 | 106 | def update_color_palette_entry(self, color_index, color_rgb, color_bw): 107 | self.color_palette[color_index] = [color_rgb, color_bw] 108 | 109 | def set_element_color(self, midiTrigger, color_idx, animation_idx): 110 | rgb, bw_rgb = self.color_palette.get(color_idx, [(255, 255, 255), (255, 255, 255)]) 111 | if rgb is None: 112 | rgb = (255, 255, 255) 113 | if bw_rgb is None: 114 | bw_rgb = (255, 255, 255) 115 | if client_connected: 116 | self.emit_ws_message('setElementColor', {'midiTrigger':midiTrigger, 'rgb': rgb, 'bwRgb': bw_rgb, 'blink': animation_idx != 0}) 117 | 118 | def prepare_and_display_in_simulator(self, frame, input_format=push2_python.constants.FRAME_FORMAT_BGR565, force=False): 119 | 120 | if time.time() - self.last_time_frame_prepared > 1.0/5.0 or force: # Limit to 5 fps to save recources 121 | self.last_time_frame_prepared = time.time() 122 | 123 | # 'frame' should be an array as in display.display_frame method input 124 | # We need to convert the frame to RGBA format first (so Pillow can read it later) 125 | 126 | if input_format == push2_python.constants.FRAME_FORMAT_RGB: 127 | frame = push2_python.display.rgb_to_bgr565(frame) 128 | 129 | frame = frame.transpose().flatten() 130 | rgb_frame = numpy.zeros(shape=(len(frame), 1), dtype=numpy.uint32).flatten() 131 | rgb_frame[:] = frame[:] 132 | 133 | if input_format == push2_python.constants.FRAME_FORMAT_RGB565: 134 | r_filter = int('1111100000000000', 2) 135 | g_filter = int('0000011111100000', 2) 136 | b_filter = int('0000000000011111', 2) 137 | frame_r_filtered = numpy.bitwise_and(rgb_frame, r_filter) 138 | frame_r_shifted = numpy.right_shift(frame_r_filtered, 8) # Shift 8 to right so R sits in the the 0-7 right-most bits 139 | frame_g_filtered = numpy.bitwise_and(rgb_frame, g_filter) 140 | frame_g_shifted = numpy.left_shift(frame_g_filtered, 5) # Shift 5 to the left so G sits at the 8-15 bits 141 | frame_b_filtered = numpy.bitwise_and(rgb_frame, b_filter) 142 | frame_b_shifted = numpy.left_shift(frame_b_filtered, 19) # Shift 19 to the left so G sits at the 16-23 bits 143 | rgb_frame = frame_r_shifted + frame_g_shifted + frame_b_shifted # Combine all channels 144 | rgb_frame = numpy.bitwise_or(rgb_frame, int('11111111000000000000000000000000', 2)) # Set alpha channel to "full!" (bits 24-32) 145 | 146 | elif input_format == push2_python.constants.FRAME_FORMAT_BGR565 or input_format == push2_python.constants.FRAME_FORMAT_RGB: 147 | r_filter = int('0000000000011111', 2) 148 | g_filter = int('0000011111100000', 2) 149 | b_filter = int('1111100000000000', 2) 150 | frame_r_filtered = numpy.bitwise_and(rgb_frame, r_filter) 151 | frame_r_shifted = numpy.left_shift(frame_r_filtered, 3) # Shift 3 to left so R sits in the the 0-7 right-most bits 152 | frame_g_filtered = numpy.bitwise_and(rgb_frame, g_filter) 153 | frame_g_shifted = numpy.left_shift(frame_g_filtered, 5) # Shift 5 to the left so G sits at the 8-15 bits 154 | frame_b_filtered = numpy.bitwise_and(rgb_frame, b_filter) 155 | frame_b_shifted = numpy.left_shift(frame_b_filtered, 8) # Shift 8 to the left so G sits at the 16-23 bits 156 | rgb_frame = frame_r_shifted + frame_g_shifted + frame_b_shifted # Combine all channels 157 | rgb_frame = numpy.bitwise_or(rgb_frame, int('11111111000000000000000000000000', 2)) # Set alpha channel to "full!" (bits 24-32) 158 | 159 | img = Image.frombytes('RGBA', (960, 160), rgb_frame.tobytes()) 160 | buffered = BytesIO() 161 | img.save(buffered, format="png") 162 | base64Image = 'data:image/png;base64, ' + str(base64.b64encode(buffered.getvalue()))[2:-1] 163 | self.emit_ws_message('setDisplay', {'base64Image': base64Image}) 164 | 165 | 166 | @sim_app.on('connect') 167 | def test_connect(): 168 | global client_connected 169 | client_connected = True 170 | push_object.trigger_action(push2_python.constants.ACTION_MIDI_CONNECTED) 171 | push_object.trigger_action(push2_python.constants.ACTION_DISPLAY_CONNECTED) 172 | logging.info('Simulator client connected') 173 | 174 | 175 | @sim_app.on('disconnect') 176 | def test_disconnect(): 177 | global client_connected 178 | client_connected = False 179 | push_object.trigger_action(push2_python.constants.ACTION_MIDI_DISCONNECTED) 180 | push_object.trigger_action(push2_python.constants.ACTION_DISPLAY_DISCONNECTED) 181 | logging.info('Simulator client disconnected') 182 | 183 | 184 | @sim_app.on('getPendingMessages') 185 | def get_ws_messages_from_queue(): 186 | push_object.simulator_controller.emit_messages_from_ws_queue() 187 | 188 | 189 | @sim_app.on('padPressed') 190 | def pad_pressed(midiTrigger): 191 | msg = make_midi_message_from_midi_trigger(midiTrigger) 192 | if midi_out is not None: 193 | midi_out.send(msg) 194 | push_object.pads.on_midi_message(msg) 195 | 196 | 197 | @sim_app.on('padReleased') 198 | def pad_released(midiTrigger): 199 | msg = make_midi_message_from_midi_trigger(midiTrigger, releasing=True) 200 | if midi_out is not None: 201 | midi_out.send(msg) 202 | push_object.pads.on_midi_message(msg) 203 | 204 | 205 | @sim_app.on('buttonPressed') 206 | def button_pressed(midiTrigger): 207 | msg = make_midi_message_from_midi_trigger(midiTrigger) 208 | if midi_out is not None: 209 | midi_out.send(msg) 210 | push_object.buttons.on_midi_message(msg) 211 | 212 | 213 | @sim_app.on('buttonReleased') 214 | def button_released(midiTrigger): 215 | msg = make_midi_message_from_midi_trigger(midiTrigger, releasing=True) 216 | if midi_out is not None: 217 | midi_out.send(msg) 218 | push_object.buttons.on_midi_message(msg) 219 | 220 | 221 | @sim_app.on('encdoerTouched') 222 | def encoder_pressed(midiTrigger): 223 | msg = make_midi_message_from_midi_trigger(midiTrigger, velocity=127) 224 | if midi_out is not None: 225 | midi_out.send(msg) 226 | push_object.encoders.on_midi_message(msg) 227 | 228 | 229 | @sim_app.on('encdoerReleased') 230 | def encoder_released(midiTrigger): 231 | msg = make_midi_message_from_midi_trigger(midiTrigger, velocity=0) 232 | if midi_out is not None: 233 | midi_out.send(msg) 234 | push_object.encoders.on_midi_message(msg) 235 | 236 | 237 | @sim_app.on('encdoerRotated') 238 | def encoder_rotated(midiTrigger, value): 239 | msg = make_midi_message_from_midi_trigger(midiTrigger, value=value) 240 | if midi_out is not None: 241 | midi_out.send(msg) 242 | push_object.encoders.on_midi_message(msg) 243 | 244 | 245 | @app.route('/') 246 | def index(): 247 | return render_template('index.html') 248 | 249 | 250 | def run_simulator_in_thread(port): 251 | global app_thread_id 252 | app_thread_id = threading.get_ident() 253 | logging.error('Running simulator at http://localhost:{}'.format(port)) 254 | sim_app.run(app, port=port) 255 | 256 | 257 | def start_simulator(_push_object, port, use_virtual_midi_out): 258 | global push_object, midi_out 259 | push_object = _push_object 260 | if use_virtual_midi_out: 261 | name = 'Push2Simulator' 262 | logging.info('Sending Push2 simulated messages to "{}" virtual midi output'.format(name)) 263 | midi_out = mido.open_output(name, virtual=True) 264 | thread = Thread(target=run_simulator_in_thread, args=(port, )) 265 | thread.start() 266 | return SimulatorController() 267 | -------------------------------------------------------------------------------- /push2_python/pads.py: -------------------------------------------------------------------------------- 1 | import mido 2 | from .constants import ANIMATION_DEFAULT, MIDO_NOTEON, MIDO_NOTEOFF, \ 3 | MIDO_POLYAT, MIDO_AFTERTOUCH, ACTION_PAD_PRESSED, ACTION_PAD_RELEASED, ACTION_PAD_AFTERTOUCH, PUSH2_SYSEX_PREFACE_BYTES, \ 4 | PUSH2_SYSEX_END_BYTES, ANIMATION_STATIC 5 | from .classes import AbstractPush2Section 6 | 7 | 8 | def pad_ij_to_pad_n(i, j): 9 | """Transform (i, j) coordinates to the corresponding pad number 10 | according to the specification. (0, 0) corresponds to the top-left pad while 11 | (7, 7) corresponds to the bottom right pad. 12 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#23-midi-mapping 13 | """ 14 | 15 | def clamp(value, minv, maxv): 16 | return max(minv, min(value, maxv)) 17 | 18 | return 92 - (clamp(i, 0, 7) * 8) + clamp(j, 0, 7) 19 | 20 | 21 | def pad_n_to_pad_ij(n): 22 | """Transform MIDI note number to pad (i, j) coordinates. 23 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#23-midi-mapping 24 | """ 25 | return (99 - n) // 8, 7 - (99 - n) % 8 26 | 27 | 28 | def get_individual_pad_action_name(action_name, pad_n=None, pad_ij=None): 29 | n = pad_n 30 | if pad_n is None: 31 | n = pad_ij_to_pad_n(pad_ij[0], pad_ij[1]) 32 | return '{0} - {1}'.format(action_name, n) 33 | 34 | 35 | class Push2Pads(AbstractPush2Section): 36 | """Class to interface with Ableton's Push2 pads. 37 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#Pads 38 | """ 39 | 40 | current_pads_state = dict() 41 | 42 | def reset_current_pads_state(self): 43 | """This function resets the stored pads state to avoid Push2 pads becoming out of sync with the push2-midi stored state. 44 | This only applies if "optimize_num_messages" is used in "set_pad_color" as it would stop sending a message if the 45 | desired color is already the one listed in the internal state. 46 | """ 47 | self.current_pads_state = dict() 48 | 49 | 50 | def set_polyphonic_aftertouch(self): 51 | """Set pad aftertouch mode to polyphonic aftertouch 52 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#285-aftertouch 53 | """ 54 | msg = mido.Message.from_bytes(PUSH2_SYSEX_PREFACE_BYTES + [0x1E, 0x01] + PUSH2_SYSEX_END_BYTES) 55 | self.push.send_midi_to_push(msg) 56 | 57 | def set_channel_aftertouch(self): 58 | """Set pad aftertouch mode to channel aftertouch 59 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#285-aftertouch 60 | """ 61 | msg = mido.Message.from_bytes(PUSH2_SYSEX_PREFACE_BYTES + [0x1E, 0x00] + PUSH2_SYSEX_END_BYTES) 62 | self.push.send_midi_to_push(msg) 63 | 64 | 65 | def set_channel_aftertouch_range(self, range_start=401, range_end=2048): 66 | """Configures the sensitivity of channel aftertouch by defining at what "range start" pressure value the aftertouch messages 67 | start to be triggered and what "range end" pressure value corresponds to the aftertouch value 127. I'm not sure about the meaning 68 | of the pressure values, but according to the documentation must be between 400 and 2048. 69 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#282-pad-parameters 70 | """ 71 | assert type(range_start) == int and type(range_end) == int, "range_start and range_end must be int" 72 | assert range_start < range_end, "range_start must be lower than range_end" 73 | assert 400 < range_start < range_end, "wrong range_start value, must be in range [401, range_end]" 74 | assert range_start < range_end <= 2048, "wrong range_end value, must be in range [range_start + 1, 2048]" 75 | lower_range_bytes = [range_start % 2**7, range_start // 2**7] 76 | upper_range_bytes = [range_end % 2**7, range_end // 2**7] 77 | msg = mido.Message.from_bytes(PUSH2_SYSEX_PREFACE_BYTES + [0x1B, 0x00, 0x00, 0x00, 0x00] + lower_range_bytes + upper_range_bytes + PUSH2_SYSEX_END_BYTES) 78 | self.push.send_midi_to_push(msg) 79 | 80 | 81 | def set_velocity_curve(self, velocities): 82 | """Configures Push pad's velocity curve which will determine i) the velocity values triggered when pressing pads; and ii) the 83 | sensitivity of the aftertouch when in polyphonic aftertouch mode. Push uses a map of physical pressure values [0g..4095g] 84 | to MIDI velocity values [0..127]. This map is quantized into 128 steps which Push then interpolates. This method expects a list of 85 | 128 velocity values which will be assigned to each of the 128 quantized steps of the physical pressure range [0g..4095g]. 86 | See hhttps://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#281-velocity-curve 87 | """ 88 | assert type(velocities) == list and len(velocities) == 128 and type(velocities[0] == int), "velocities must be a list with 128 int values" 89 | for start_index in range(0, 128, 16): 90 | msg = mido.Message.from_bytes(PUSH2_SYSEX_PREFACE_BYTES + [0x20] + [start_index] + velocities[start_index:start_index + 16] + PUSH2_SYSEX_END_BYTES) 91 | self.push.send_midi_to_push(msg) 92 | 93 | def pad_ij_to_pad_n(self, i, j): 94 | return pad_ij_to_pad_n(i, j) 95 | 96 | def pad_n_to_pad_ij(self, n): 97 | return pad_n_to_pad_ij(n) 98 | 99 | def set_pad_color(self, pad_ij, color='white', animation=ANIMATION_DEFAULT, optimize_num_messages=True, animation_end_color='black'): 100 | """Sets the color of the pad at the (i,j) coordinate. 101 | 'color' must be a valid RGB color name present in the color palette. See push2_python.constants.DEFAULT_COLOR_PALETTE for default color names. 102 | 'animation' must be a valid animation name from those defined in push2_python.contants.ANIMATION_*. Note that to configure an animation, both 103 | the 'start' and 'end' colors of the animation need to be defined. The 'start' color is defined by 'color' parameter. The 'end' color is defined 104 | by the color specified in 'animation_end_color', which must be a valid RGB color name present in the color palette. 105 | 106 | This funtion will keep track of the latest color/animation values set for each specific pad. If 'optimize_num_messages' is 107 | set to True, set_pad_color will only actually send the MIDI message to push if either the color or animation that should 108 | be set differ from those stored in the state. 109 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#261-setting-led-colors 110 | """ 111 | pad = self.pad_ij_to_pad_n(pad_ij[0], pad_ij[1]) 112 | color = self.push.get_rgb_color(color) 113 | if optimize_num_messages and pad in self.current_pads_state and self.current_pads_state[pad]['color'] == color and self.current_pads_state[pad]['animation'] == animation: 114 | # If pad's recorded state already has the specified color and animation, return method before sending the MIDI message 115 | return 116 | if animation != ANIMATION_STATIC: 117 | # If animation is not static, we first set the pad to black color with static animation so then, when setting 118 | # the desired color with the corresponding animation it lights as expected. 119 | msg = mido.Message(MIDO_NOTEON, note=pad, velocity=self.push.get_rgb_color(animation_end_color), channel=ANIMATION_STATIC) 120 | self.push.send_midi_to_push(msg) 121 | msg = mido.Message(MIDO_NOTEON, note=pad, velocity=color, channel=animation) 122 | self.push.send_midi_to_push(msg) 123 | self.current_pads_state[pad] = {'color': color, 'animation': animation} 124 | 125 | if self.push.simulator_controller is not None: 126 | self.push.simulator_controller.set_element_color('nn' + str(pad), color, animation) 127 | 128 | def set_pads_color(self, color_matrix, animation_matrix=None): 129 | """Sets the color and animations of all pads according to the given matrices. 130 | Individual elements in the color_matrix must be valid RGB color palette names. See push2_python.constants.DEFAULT_COLOR_PALETTE for default color names. 131 | Matrices must be 8x8, with 8 lines of 8 values corresponding to the pad grid from top-left to bottom-down. 132 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#261-setting-led-colors 133 | """ 134 | assert len(color_matrix) == 8, 'Wrong number of lines in color matrix ({0})'.format(len(color_matrix)) 135 | if animation_matrix is not None: 136 | assert len(animation_matrix) == 8, 'Wrong number of lines in animation matrix ({0})'.format(len(animation_matrix)) 137 | for i, line in enumerate(color_matrix): 138 | assert len(line) == 8, 'Wrong number of color values in line ({0})'.format(len(line)) 139 | if animation_matrix is not None: 140 | assert len(animation_matrix[i]) == 8, 'Wrong number of animation values in line ({0})'.format(len(animation_matrix[i])) 141 | for j, color in enumerate(line): 142 | animation = ANIMATION_DEFAULT 143 | animation_end_color = 'black' 144 | if animation_matrix is not None: 145 | element = animation_matrix[i][j] 146 | if type(element) == tuple: 147 | animation, animation_end_color = animation_matrix[i][j] 148 | else: 149 | animation = animation_matrix[i][j] 150 | self.set_pad_color((i, j), color=color, animation=animation, animation_end_color=animation_end_color) 151 | 152 | def set_all_pads_to_color(self, color='white', animation=ANIMATION_DEFAULT, animation_end_color='black'): 153 | """Set all pads to the given color/animation. 154 | 'color' must be a valid RGB color name present in the color palette. See push2_python.constants.DEFAULT_COLOR_PALETTE for default color names. 155 | 'animation' must be a valid animation name from those defined in push2_python.contants.ANIMATION_*. Note that to configure an animation, both the 'start' and 'end' 156 | colors of the animation need to be defined. The 'start' color is defined by setting a color with 'push2_python.contants.ANIMATION_STATIC' (the default). 157 | The second color is set setting a color with whatever ANIMATION_* type is desired. 158 | """ 159 | color_matrix = [[color for _ in range(0, 8)] for _ in range(0, 8)] 160 | animation_matrix = [[(animation, animation_end_color) for _ in range(0, 8)] for _ in range(0, 8)] 161 | self.set_pads_color(color_matrix, animation_matrix) 162 | 163 | def set_all_pads_to_black(self, animation=ANIMATION_DEFAULT, animation_end_color='black'): 164 | self.set_all_pads_to_color('black', animation=animation, animation_end_color=animation_end_color) 165 | 166 | def set_all_pads_to_white(self, animation=ANIMATION_DEFAULT, animation_end_color='black'): 167 | self.set_all_pads_to_color('white', animation=animation, animation_end_color=animation_end_color) 168 | 169 | def set_all_pads_to_red(self, animation=ANIMATION_DEFAULT, animation_end_color='black'): 170 | self.set_all_pads_to_color('red', animation=animation, animation_end_color=animation_end_color) 171 | 172 | def set_all_pads_to_green(self, animation=ANIMATION_DEFAULT, animation_end_color='black'): 173 | self.set_all_pads_to_color('green', animation=animation, animation_end_color=animation_end_color) 174 | 175 | def set_all_pads_to_blue(self, animation=ANIMATION_DEFAULT, animation_end_color='black'): 176 | self.set_all_pads_to_color('blue', animation=animation, animation_end_color=animation_end_color) 177 | 178 | def on_midi_message(self, message): 179 | if message.type in [MIDO_NOTEON, MIDO_NOTEOFF, MIDO_POLYAT, MIDO_AFTERTOUCH]: 180 | if message.type != MIDO_AFTERTOUCH: 181 | if 36 <= message.note <= 99: # Min and max pad MIDI values according to Push Spec 182 | pad_n = message.note 183 | pad_ij = self.pad_n_to_pad_ij(pad_n) 184 | if message.type == MIDO_POLYAT: 185 | velocity = message.value 186 | else: 187 | velocity = message.velocity 188 | if message.type == MIDO_NOTEON: 189 | self.push.trigger_action(ACTION_PAD_PRESSED, pad_n, pad_ij, velocity) # Trigger generic pad action 190 | self.push.trigger_action(get_individual_pad_action_name( 191 | ACTION_PAD_PRESSED, pad_n=pad_n), velocity) # Trigger individual pad action as well 192 | return True 193 | elif message.type == MIDO_NOTEOFF: 194 | self.push.trigger_action(ACTION_PAD_RELEASED, pad_n, pad_ij, velocity) 195 | self.push.trigger_action(get_individual_pad_action_name( 196 | ACTION_PAD_RELEASED, pad_n=pad_n), velocity) # Trigger individual pad action as well 197 | return True 198 | elif message.type == MIDO_POLYAT: 199 | self.push.trigger_action(ACTION_PAD_AFTERTOUCH, pad_n, pad_ij, velocity) 200 | self.push.trigger_action(get_individual_pad_action_name( 201 | ACTION_PAD_AFTERTOUCH, pad_n=pad_n), velocity) # Trigger individual pad action as well 202 | return True 203 | elif message.type == MIDO_AFTERTOUCH: 204 | self.push.trigger_action(ACTION_PAD_AFTERTOUCH, None, None, message.value) 205 | return True 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # push2-python 2 | 3 | Utils to interface with [Ableton's Push 2](https://www.ableton.com/en/push/) from Python. 4 | 5 | These utils follow Ableton's [Push 2 MIDI and Display Interface Manual](https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc) for comunicating with Push 2. I recommend reading Ableton's manual before using this tool. 6 | 7 | So far I only implemented some utils to **interface with the display** and some utils for **interaction with pads, buttons, encoders and the touchstrip**. More detailed interaction with each of these elements (e.g. changing color palettes, support for led blinking, advanced touchstrip configuration, etc.) has not been implemented. Contributions are welcome :) 8 | **UPDATE**: customization of color palettes and led animations is now implemented! 9 | 10 | I only testd the package in **Python 3** and **macOS**. Some things will not work on Python 2 but it should be easy to port. I don't know how it will work on Windows/Linux. ~~It is possible that MIDI port names (see [push2_python/constants.py](https://github.com/ffont/push2-python/blob/master/push2_python/constants.py#L12-L13)) need to be changed to correctly reach Push2 in Windows/Linux~~. **UPDATE**: MIDI port names should now be cross-platform, but I have not tested them on Linux/Windows. 11 | 12 | `push2-python` incorporates a Push2 simulator so you can do development without having your push connected. Check out the [simulator section](#using-the-simulator) below 13 | 14 | Code examples are shown at the end of this readme file. For an example of a full application that I built using `push2-python` and that allows you to turn your Push2 into a standalone MIDI controller (using a Rapsberry Pi!), check the [Pysha](https://github.com/ffont/pysha) source source code repository. 15 | 16 | 17 | ## Table of Contents 18 | 19 | * [Install](#install) 20 | * [Documentation](#documentation) 21 | * [Initializing Push](#initializing-push) 22 | * [Setting action handlers for buttons, encoders, pads and the touchstrip](#setting-action-handlers-for-buttons--encoders--pads-and-the-touchstrip) 23 | * [Button names, encoder names, pad numbers and coordinates](#button-names--encoder-names--pad-numbers-and-coordinates) 24 | * [Set pad and button colors](#set-pad-and-button-colors) 25 | * [Interface with the display](#interface-with-the-display) 26 | * [Using the simulator](#using-the-simulator) 27 | * [Code examples](#code-examples) 28 | * [Set up handlers for pads, encoders, buttons and the touchstrip...](#set-up-handlers-for-pads-encoders-buttons-and-the-touchstrip) 29 | * [Light up buttons and pads](#light-up-buttons-and-pads) 30 | * [Interface with the display (static content)](#interface-with-the-display-static-content) 31 | * [Interface with the display (dynamic content)](#interface-with-the-display-dynamic-content) 32 | 33 | 34 | ## Install 35 | 36 | You can install using `pip` and pointing at this repository: 37 | 38 | ``` 39 | pip install git+https://github.com/ffont/push2-python 40 | ``` 41 | 42 | This will install Python requirements as well. Note however that `push2-python` requires [pyusb](https://github.com/pyusb/pyusb) which is based in [libusb](https://libusb.info/). You'll most probably need to manually install `libusb` for your operative system if `pip` does not do it for you. 43 | 44 | ## Documentation 45 | 46 | Well, to be honest there is no proper documentation. However the use of this package is so simple that I hope it's going to be enough with the [code examples below](#code-examples) and the simple notes given here. 47 | 48 | ### Initializing Push 49 | 50 | To interface with Push2 you'll first need to import `push2_python` and initialize a Python object as follows: 51 | 52 | ```python 53 | import push2_python 54 | 55 | push = push2_python.Push2() 56 | ``` 57 | 58 | **NOTE**: all code snippets below assume you import `push2_python` and initialize the `Push2` like in the snippet above. 59 | 60 | You can pass the optional argument `use_user_midi_port=True` when initializing `push` to tell it to use User MIDI port instead of Live MIDI port. Check [MIDI interface access](https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#midi-interface-access) and [MIDI mode](https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#MIDI%20Mode) sections of the [Push 2 MIDI and Display Interface Manual](https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc) for more information. 61 | 62 | When `push2_python.Push2()` is run, `push2_python` tries to set up MIDI in connection with Push2 so it can start receiving incomming MIDI in messages (e.g. if a pad is pressed). MIDI out connection and display connection are lazily configured the first time a frame is sent to the display or a MIDI message is sent to Push2 (e.g. to light a pad). If `push2_python.Push2()` is run while Push2 is powered off, it won't be able to automatically detect when it is powered on to automatically configure connection. Nevertheless, if a frame is sent to Push2's display or any MIDI message is sent after it has been powered on, then configuration will happen automatically and should work as expected. For the specific case of MIDI connection, after a connection has been first set up then `push2_python` will be able to detect when Push2 gets powered off and on by tracking *active sense* messages sent by Push2. In summary, if you want to build an app that can automatically connect to Push2 when it becomes available and/or recover from Push2 temporarily being unavailable we recommend that you have some sort of main loop that keeps trying to send frames to Push2 display (if you want to make use of the display) and/or keeps trying to configure Push2 MIDI. As an example: 63 | 64 | ```python 65 | import time 66 | import push2_python 67 | 68 | push = push2_python.Push2() # Call this while Push2 is still powered off 69 | while True: # This is your app's main loop 70 | 71 | # Try to send some frame to Push2 display to force display connection/reconnection 72 | frame = generate_frame_for_push_display() # Some fake function to do that 73 | push.display.display_frame(frame) 74 | 75 | # Try to configure Push2 MIDI at every iteration (if not already configured) 76 | if not push.midi_is_configured(): 77 | push.configure_midi() 78 | 79 | time.sleep(0.1) 80 | ``` 81 | 82 | **NOTE 1**: This calls must be done from your app's main thread (where `push2_python.Push2()` is run). Maybe it is possible 83 | to delegate all connection with `push2_python` to a different thread (have not tried that), but it is important that all 84 | MIDI configuration calls happen in the same thread because of limitations of the `mido` Python MIDI package used by `push2_python`. 85 | 86 | **NOTE 2**: The solution above is only needed if you want to support Push2 being powered off when your app starts. After your app connects successfuly with Push2, the recurring check for MIDI configuration would not really be needed because `push2_python` will keep track of MIDI connections using active sensing. 87 | 88 | 89 | ### Setting action handlers for buttons, encoders, pads and the touchstrip 90 | 91 | You can easily set action handlers that will trigger functions when the physical pads, buttons, encoders or the touchstrip are used. You do that by **decorating functions** that will be triggered in response to the physical actions. For example, you can set up an action handler that will be triggered when 92 | the left-most encoder is rotated in this way: 93 | 94 | ```python 95 | @push2_python.on_encoder_rotated(push2_python.constants.ENCODER_TEMPO_ENCODER) 96 | def on_left_encoder_rotated(push, increment): 97 | print('Left-most encoder rotated with increment', increment) 98 | ``` 99 | 100 | Similarly, you can set up an action handler that will trigger when play button is pressed in this way: 101 | 102 | ```python 103 | @push2_python.on_button_pressed(push2_python.constants.BUTTON_PLAY) 104 | def on_play_pressed(push): 105 | print('Play!') 106 | ``` 107 | 108 | These are all available decorators for setting up action handlers: 109 | 110 | * `@push2_python.on_button_pressed(button_name=None)` 111 | * `@push2_python.on_button_released(button_name=None)` 112 | * `@push2_python.on_touchstrip()` 113 | * `@push2_python.on_pad_pressed(pad_n=None, pad_ij=None)` 114 | * `@push2_python.on_pad_released(pad_n=None, pad_ij=None)` 115 | * `@push2_python.on_pad_aftertouch(pad_n=None, pad_ij=None)` 116 | * `@push2_python.on_encoder_rotated(encoder_name=None)` 117 | * `@push2_python.on_encoder_touched(encoder_name=None)` 118 | * `@push2_python.on_encoder_released(encoder_name=None)` 119 | * `@push2_python.on_display_connected()` 120 | * `@push2_python.on_display_disconnected()` 121 | * `@push2_python.on_midi_connected()` 122 | * `@push2_python.on_midi_disconnected()` 123 | * `@push2_python.on_sustain_pedal()` 124 | 125 | Full documentation for each of these can be found in their docstrings [starting here](https://github.com/ffont/push2-python/blob/master/push2_python/__init__.py#L128). 126 | Also have a look at the [code examples](#code-examples) below to get an immediate idea about how it works. 127 | 128 | 129 | ### Button names, encoder names, pad numbers and coordinates 130 | 131 | Buttons and encoders can de identified by their name. You can get a list of avialable options for `button_name` and `encoder_name` by checking the 132 | contents of [push2_python/constants.py](https://github.com/ffont/push2-python/blob/master/push2_python/constants.py) 133 | or by using the following properties after intializing the `Push2` object: 134 | 135 | ```python 136 | print(push.buttons.available_names) 137 | print(push.encoders.available_names) 138 | ``` 139 | 140 | Pads are identified either by their number (`pad_n`) or by their coordinates (`pad_ij`). Pad numbers correspond to the MIDI note numbers assigned 141 | to each pad as defined in [Push 2 MIDI and Display Interface Manual](https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#23-midi-mapping) (see MIDI mapping diagram). Pad coordinates are specified as a `(i,j)` tuples where `(0,0)` corresponds to the top-left pad and `(7, 7)` corresponds to the bottom right pad. 142 | 143 | ### Set pad and button colors 144 | 145 | Pad and button colors can be set using methods provided by the `Push2` object. For example you can set pad colors using the following code: 146 | 147 | ```python 148 | pad_ij = (0, 3) # Fourth pad of the top row 149 | push.pads.set_pad_color(pad_ij, 'green') 150 | ``` 151 | 152 | You set button colors in a similar way: 153 | 154 | ```python 155 | push.buttons.set_button_color(push2_python.constants.BUTTON_PLAY, 'green') 156 | ``` 157 | 158 | All pads support RGB colors, and some buttons do as well. However, some buttons only support black and white. Checkout the MIDI mapping diagram in the 159 | [Push 2 MIDI and Display Interface Manual](https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#23-midi-mapping) to see which buttons support RGB and which ones only support black and white. In both cases colors are set using the same method, but the list of available colors for black and white buttons is restricted. 160 | 161 | For a list of avilable RGB colors check the `DEFAULT_COLOR_PALETTE` dictionary in [push2_python/constants.py](https://github.com/ffont/push2-python/blob/master/push2_python/constants.py). First item of each color entry corresponds to the RGB color name while second item corresponds to the BW color name. The color palette can be customized using the `set_color_palette_entry`, `update_rgb_color_palette_entry` and `reapply_color_palette` of Push2 object. See the documentation of these methods for more details. 162 | 163 | 164 | ### Set pad and button animations 165 | 166 | Animations (e.g. led blinking) can be configured similarly to colors. To configiure an animation you need to define the *starting color* and the *ending color* plus the type of animation. For example, to configure the play button with a pulsing animation from green to white: 167 | 168 | ```python 169 | push.buttons.set_button_color(push2_python.constants.BUTTON_PLAY, 'green', animation=push2_python.constants.ANIMATION_PULSING_QUARTER, animation_end_color='white') 170 | ``` 171 | 172 | By default, animations are synced to a clock of 120bpm. It is possible to change that tempo by sending MIDI clock messages to the Push2 device, but `push2-python` currently does not support that. Should be easy to implement though by sending MIDI clock messages using the `push.send_midi_to_push(msg)` method. 173 | 174 | For a list of available animations, check the variables names `ANIMATION_*` dictionary in [push2_python/constants.py](https://github.com/ffont/push2-python/blob/master/push2_python/constants.py). Also, see the animations section of the [Push 2 MIDI and Display Interface Manual](https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#268-led-animation) for more information about animations. 175 | 176 | 177 | ### Adjust pad sensitivity 178 | 179 | `push2-python` implements methods to adjust Push2 pads sensitivity, in particualr it incorporates methods to adjust the velocity curve (which applies to 180 | note on velocities and to poolyphonic aftertouch sensistivity), and the channel aftertouch range. You can do that using the methods `set_channel_aftertouch_range` 181 | and `set_velocity_curve` from the `pads` section. Below are two examples of adjusting sensitivity. Please check methods' documentation for more information. 182 | 183 | ```python 184 | push.pads.set_channel_aftertouch_range(range_start=401, range_end=800) # Configure channel after touch to be quite sensitive 185 | push.pads.set_velocity_curve(velocities=[int(i * 127/40) if i < 40 else 127 for i in range(0,128)]) # Map full velocity range to the first 40 pressure values 186 | ``` 187 | 188 | 189 | ### Interface with the display 190 | 191 | You interface with Push2's display by senidng frames to be display using the `push.display.display_frame` method as follows: 192 | 193 | ```python 194 | img_frame = ... # Some existing valid img_frame 195 | push.display.display_frame(img_frame, input_format=push2_python.constants.FRAME_FORMAT_BGR565) 196 | ``` 197 | 198 | `img_frame` is expected to by a `numpy` array. Depending on the `input_format` argument, `img_frame` will need to have the following characteristics: 199 | 200 | * for `push2_python.constants.FRAME_FORMAT_BGR565`: `numpy` array of shape 910x160 and of type `uint16`. Each `uint16` element specifies rgb 201 | color with the following bit position meaning: `[b4 b3 b2 b1 b0 g5 g4 g3 g2 g1 g0 r4 r3 r2 r1 r0]`. 202 | 203 | * for `push2_python.constants.FRAME_FORMAT_RGB565`: `numpy` array of shape 910x160 and of type `uint16`. Each `uint16` element specifies rgb 204 | color with the following bit position meaning: `[r4 r3 r2 r1 r0 g5 g4 g3 g2 g1 g0 b4 b3 b2 b1 b0]`. 205 | 206 | * for `push2_python.constants.FRAME_FORMAT_RGB`: numpy array of shape 910x160x3 with the third dimension representing rgb colors 207 | with separate float values for rgb channels (float values in range `[0.0, 1.0]`). 208 | 209 | The preferred format is `push2_python.constants.FRAME_FORMAT_BGR565` as it requires no conversion before sending to Push2 (that is the format that Push2 expects). Using `push2_python.constants.FRAME_FORMAT_BGR565` it should be possible to achieve frame rates of more than 36fps (depending on the speed of your computer). 210 | With `push2_python.constants.FRAME_FORMAT_RGB565` we need to convert the frame to `push2_python.constants.FRAME_FORMAT_BGR565` before sending to Push2. This will reduce frame rates to ~14fps (allways depending on the speed of your computer). Sending data in `push2_python.constants.FRAME_FORMAT_RGB` will result in very long frame conversion times that can take seconds. This format should only be used for displaying static images that are prepared offline using the `push.display.prepare_frame` method. The code examples below ([here](#interface-with-the-display-static-content) and [here](#interface-with-the-display-dynamic-content)) should give you an idea of how this works. It's easy! 211 | 212 | **NOTE 1**: According to Push2 display specification, when you send a frame to Push2, it will stay on screen for two seconds. Then the screen will go to black. 213 | 214 | **NOTE 2**: Interfacing with the display using `push2-python` won't allow you to get very high frame rates, but it should be enough for most applications. If you need to make more hardcore use of the display you should probably implement your own funcions directly in C or C++. Push2's display theoretically supports up to 60fps. More information in the [Push 2 MIDI and Display Interface Manual](https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#32-display-interface-protocol). 215 | 216 | ### Using the simulator 217 | 218 | `push2-python` bundles a browser-based Push2 simulator that you can use for doing development while away from your Push. To use the simulator, you just need to initialize `Push2` in the following way: 219 | 220 | ``` 221 | push = push2_python.Push2(run_simulator=True) 222 | ``` 223 | 224 | And then, while your app is running, point your browser at `localhost:6128`. Here is a screenshot of the simulator in action: 225 | 226 |
227 |
228 |
Use shift+click to hold buttons/pads pressed. In encoders, use shift+click in the arrow keys to rotate with bigger increments.
578 |