├── .gitignore ├── LICENSE ├── README.md ├── push2_python ├── __init__.py ├── buttons.py ├── classes.py ├── constants.py ├── display.py ├── encoders.py ├── exceptions.py ├── pads.py ├── push2_map.py ├── simulator │ ├── __init__.py │ ├── simulator.py │ └── templates │ │ └── index.html └── touchstrip.py ├── setup.py └── 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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 |

229 | 230 | You can customize the port that the simulator uses by passing `simulator_port` argument when initializing `push2_python.Push2`. Note that the **simulator only implements basic functionality** of Push2, and has some important limitations. For instance, the FPS of the display is limited. Also pressing/releasing buttons or pads very fast may result in some cases in "lost" messages. Touchstrip support is not implemented nor pressure sentisitivy in the pads. You can however use the simulator to trigger buttons and pads, rotate and touch/release encoders, show the display and set pad/button colors. Color palettes are updated in the simulator in the same way as these are updated in Push, therefore if using configuring custom color palettes [as described above](#set-pad-and-button-colors), you should see the correct colors in the simulator. Note that the initial color palette (if no custom colors are provided) is very limited and we strongly recommend to always use a custom color palette. 231 | 232 | 233 | ## Code examples 234 | 235 | ### Set up action handlers for pads, encoders, buttons and the touchstrip... 236 | 237 | ```python 238 | import push2_python 239 | 240 | # Init Push2 241 | push = push2_python.Push2() 242 | 243 | # Now set up some action handlers that will trigger when interacting with Push2 244 | # This is all done using decorators. 245 | @push2_python.on_pad_pressed() 246 | def on_pad_pressed(push, pad_n, pad_ij, velocity): 247 | print('Pad', pad_ij, 'pressed with velocity', velocity) 248 | 249 | @push2_python.on_encoder_rotated() 250 | def on_encoder_rotated(push, encoder_name, increment): 251 | print('Encoder', encoder_name, 'rotated', increment) 252 | 253 | @push2_python.on_touchstrip() 254 | def on_touchstrip(push, value): 255 | print('Touchstrip touched with value', value) 256 | 257 | # You can also set handlers for specic encoders or buttons by passing argument to the decorator 258 | @push2_python.on_encoder_rotated(push2_python.constants.ENCODER_TRACK1_ENCODER) 259 | def on_encoder1_rotated(push, incrememnt): 260 | print('Encoder for Track 1 rotated with increment', increment) 261 | 262 | @push2_python.on_button_pressed(push2_python.constants.BUTTON_1_16) 263 | def on_button_pressed(push): 264 | print('Button 1/16 pressed') 265 | 266 | # Now start infinite loop so the app keeps running 267 | print('App runnnig...') 268 | while True: 269 | pass 270 | ``` 271 | 272 | ### Light up buttons and pads 273 | 274 | ```python 275 | import push2_python 276 | 277 | # Init Push2 278 | push = push2_python.Push2() 279 | 280 | # Start by setting all pad colors to white 281 | push.pads.set_all_pads_to_color('white') 282 | 283 | @push2_python.on_button_pressed() 284 | def on_button_pressed(push, button_name): 285 | # Set pressed button color to white 286 | push.buttons.set_button_color(button_name, 'white') 287 | 288 | @push2_python.on_button_released() 289 | def on_button_released(push, button_name): 290 | # Set released button color to black (off) 291 | push.buttons.set_button_color(button_name, 'black') 292 | 293 | @push2_python.on_pad_pressed() 294 | def on_pad_pressed(push, pad_n, pad_ij, velocity): 295 | # Set pressed pad color to green 296 | push.pads.set_pad_color(pad_ij, 'green') 297 | 298 | @push2_python.on_pad_released() 299 | def on_pad_released(push, pad_n, pad_ij, velocity): 300 | # Set released pad color back to white 301 | push.pads.set_pad_color(pad_ij, 'white') 302 | 303 | # Start infinite loop so the app keeps running 304 | print('App runnnig...') 305 | while True: 306 | pass 307 | ``` 308 | 309 | ### Interface with the display (static content) 310 | 311 | Here you have some example code for interfacing with Push2's display. Note that this code example requires [`pillow`](https://python-pillow.org/) Python package, install it with `pip install pillow`. 312 | 313 | ```python 314 | import push2_python 315 | import random 316 | import numpy 317 | from PIL import Image 318 | 319 | # Init Push2 320 | push = push2_python.Push2() 321 | 322 | # Define util function to generate a frame with some colors to be shown in the display 323 | # Frames are created as matrices of shape 960x160 and with colors defined in bgr565 format 324 | # This function is defined in a rather silly way, could probably be optimized a lot ;) 325 | def generate_3_color_frame(): 326 | colors = ['{b:05b}{g:06b}{r:05b}'.format( 327 | r=int(31*random.random()), g=int(63*random.random()), b=int(31*random.random())), 328 | '{b:05b}{g:06b}{r:05b}'.format( 329 | r=int(31*random.random()), g=int(63*random.random()), b=int(31*random.random())), 330 | '{b:05b}{g:06b}{r:05b}'.format( 331 | r=int(31*random.random()), g=int(63*random.random()), b=int(31*random.random()))] 332 | colors = [int(c, 2) for c in colors] 333 | line_bytes = [] 334 | for i in range(0, 960): # 960 pixels per line 335 | if i <= 960 // 3: 336 | line_bytes.append(colors[0]) 337 | elif 960 // 3 < i <= 2 * 960 // 3: 338 | line_bytes.append(colors[1]) 339 | else: 340 | line_bytes.append(colors[2]) 341 | frame = [] 342 | for i in range(0, 160): # 160 lines 343 | frame.append(line_bytes) 344 | return numpy.array(frame, dtype=numpy.uint16).transpose() 345 | 346 | # Pre-generate different color frames 347 | color_frames = list() 348 | for i in range(0, 20): 349 | color_frames.append(generate_3_color_frame()) 350 | 351 | # Now crate an extra frame which loads an image from a file. Image must be 960x160 pixels. 352 | img = Image.open('test_img_960x160.png') 353 | frame = numpy.array(img) 354 | frame = frame/255 # Convert rgb values to [0.0, 1.0] floats 355 | 356 | # Now lets configure some action handlers which will display frames in Push2's display in 357 | # reaction to pad and button presses 358 | @push2_python.on_pad_pressed() 359 | def on_pad_pressed(push, pad_n, pad_ij, velocity): 360 | # Display one of the three color frames on the display 361 | random_frame = random.choice(color_frames) 362 | push.display.display_frame(random_frame) 363 | 364 | @push2_python.on_button_pressed() 365 | def on_button_pressed(push, button_name): 366 | # Display the frame with the loaded image 367 | push.display.display_frame(frame, input_format=push2_python.constants.FRAME_FORMAT_RGB) 368 | 369 | # Start infinite loop so the app keeps running 370 | print('App runnnig...') 371 | while True: 372 | pass 373 | ``` 374 | 375 | ### Interface with the display (dynamic content) 376 | 377 | And here is a more advanced example of interfacing with the display. In this case display frames are generated dynamically and show some values that can be modified by rotating the encoders. Note that this code example requires [`pycairo`](https://github.com/pygobject/pycairo) Python package, install it with `pip install pycairo` (you'll most probably also need to install [`cairo`](https://www.cairographics.org/) before that, see [this page](https://pycairo.readthedocs.io/en/latest/getting_started.html) for info on that). 378 | 379 | ```python 380 | import push2_python 381 | import cairo 382 | import numpy 383 | import random 384 | import time 385 | 386 | # Init Push2 387 | push = push2_python.Push2() 388 | 389 | # Init dictionary to store the state of encoders 390 | encoders_state = dict() 391 | max_encoder_value = 100 392 | for encoder_name in push.encoders.available_names: 393 | encoders_state[encoder_name] = { 394 | 'value': int(random.random() * max_encoder_value), 395 | 'color': [random.random(), random.random(), random.random()], 396 | } 397 | last_selected_encoder = list(encoders_state.keys())[0] 398 | 399 | # Function that generates the contents of the frame do be displayed 400 | def generate_display_frame(encoder_value, encoder_color, encoder_name): 401 | 402 | # Prepare cairo canvas 403 | WIDTH, HEIGHT = push2_python.constants.DISPLAY_LINE_PIXELS, push2_python.constants.DISPLAY_N_LINES 404 | surface = cairo.ImageSurface(cairo.FORMAT_RGB16_565, WIDTH, HEIGHT) 405 | ctx = cairo.Context(surface) 406 | 407 | # Draw rectangle with width proportional to encoders' value 408 | ctx.set_source_rgb(*encoder_color) 409 | ctx.rectangle(0, 0, WIDTH * (encoder_value/max_encoder_value), HEIGHT) 410 | ctx.fill() 411 | 412 | # Add text with encoder name and value 413 | ctx.set_source_rgb(1, 1, 1) 414 | font_size = HEIGHT//3 415 | ctx.set_font_size(font_size) 416 | ctx.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) 417 | ctx.move_to(10, font_size * 2) 418 | ctx.show_text("{0}: {1}".format(encoder_name, encoder_value)) 419 | 420 | # Turn canvas into numpy array compatible with push.display.display_frame method 421 | buf = surface.get_data() 422 | frame = numpy.ndarray(shape=(HEIGHT, WIDTH), dtype=numpy.uint16, buffer=buf) 423 | frame = frame.transpose() 424 | return frame 425 | 426 | # Set up action handlers to react to encoder touches and rotation 427 | @push2_python.on_encoder_rotated() 428 | def on_encoder_rotated(push, encoder_name, increment): 429 | def update_encoder_value(encoder_idx, increment): 430 | updated_value = int(encoders_state[encoder_idx]['value'] + increment) 431 | if updated_value < 0: 432 | encoders_state[encoder_idx]['value'] = 0 433 | elif updated_value > max_encoder_value: 434 | encoders_state[encoder_idx]['value'] = max_encoder_value 435 | else: 436 | encoders_state[encoder_idx]['value'] = updated_value 437 | 438 | update_encoder_value(encoder_name, increment) 439 | global last_selected_encoder 440 | last_selected_encoder = encoder_name 441 | 442 | @push2_python.on_encoder_touched() 443 | def on_encoder_touched(push, encoder_name): 444 | global last_selected_encoder 445 | last_selected_encoder = encoder_name 446 | 447 | # Draw method that will generate the frame to be shown on the display 448 | def draw(): 449 | encoder_value = encoders_state[last_selected_encoder]['value'] 450 | encoder_color = encoders_state[last_selected_encoder]['color'] 451 | frame = generate_display_frame(encoder_value, encoder_color, last_selected_encoder) 452 | push.display.display_frame(frame, input_format=push2_python.constants.FRAME_FORMAT_RGB565) 453 | 454 | # Now start infinite loop so the app keeps running 455 | print('App runnnig...') 456 | while True: 457 | draw() 458 | time.sleep(1.0/30) # Sart drawing loop, aim at ~30fps 459 | ``` 460 | -------------------------------------------------------------------------------- /push2_python/__init__.py: -------------------------------------------------------------------------------- 1 | import usb.core 2 | import usb.util 3 | import logging 4 | import sys 5 | import mido 6 | import threading 7 | import time 8 | from datetime import timedelta 9 | from collections import defaultdict 10 | from .classes import function_call_interval_limit 11 | from .exceptions import Push2USBDeviceNotFound, Push2USBDeviceConfigurationError, Push2MIDIeviceNotFound 12 | from .display import Push2Display 13 | from .pads import Push2Pads, get_individual_pad_action_name 14 | from .buttons import Push2Buttons, get_individual_button_action_name 15 | from .encoders import Push2Encoders, get_individual_encoder_action_name 16 | from .touchstrip import Push2TouchStrip 17 | from .push2_map import push2_map 18 | from .constants import is_push_midi_in_port_name, is_push_midi_out_port_name, PUSH2_MAP_FILE_PATH, ACTION_BUTTON_PRESSED, \ 19 | ACTION_BUTTON_RELEASED, ACTION_TOUCHSTRIP_TOUCHED, ACTION_PAD_PRESSED, ACTION_PAD_RELEASED, ACTION_PAD_AFTERTOUCH, \ 20 | ACTION_ENCODER_ROTATED, ACTION_ENCODER_TOUCHED, ACTION_ENCODER_RELEASED, PUSH2_RECONNECT_INTERVAL, ACTION_DISPLAY_CONNECTED, \ 21 | ACTION_DISPLAY_DISCONNECTED, ACTION_MIDI_CONNECTED, ACTION_MIDI_DISCONNECTED, PUSH2_MIDI_ACTIVE_SENSING_MAX_INTERVAL, ACTION_SUSTAIN_PEDAL, \ 22 | MIDO_CONTROLCHANGE, PUSH2_SYSEX_PREFACE_BYTES, PUSH2_SYSEX_END_BYTES, DEFAULT_COLOR_PALETTE, DEFAULT_RGB_COLOR, DEFAULT_BW_COLOR 23 | 24 | from .simulator.simulator import start_simulator 25 | 26 | logging.basicConfig(stream=sys.stdout, level=logging.ERROR) 27 | 28 | action_handler_registry = defaultdict(list) 29 | 30 | 31 | class Push2(object): 32 | """Class to interface with Ableton's Push2. 33 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc 34 | """ 35 | midi_in_port = None 36 | midi_out_port = None 37 | push2_map = None 38 | display = None 39 | pads = None 40 | buttons = None 41 | encoders = None 42 | touchstrip = None 43 | use_user_midi_port = False 44 | last_active_sensing_received = None 45 | function_call_interval_limit_overwrite = PUSH2_RECONNECT_INTERVAL 46 | color_palette = DEFAULT_COLOR_PALETTE.copy() 47 | simulator_controller = None 48 | 49 | 50 | def __init__(self, use_user_midi_port=False, run_simulator=False, simulator_port=6128, simulator_use_virtual_midi_out=False): 51 | """Initializes object to interface with Ableton's Push2. 52 | This function will set up USB and MIDI connections with the hardware device. 53 | By default, MIDI connection will use LIVE MIDI port instead of USER MIDI port. 54 | USER MIDI port can be configured using the argument 'use_user_midi_port'. Alternatively 55 | a custom specific MIDI port name for Push can be provided using the argument 'push_midi_port_name'. 56 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc 57 | """ 58 | 59 | self.use_user_midi_port = use_user_midi_port 60 | 61 | # Load Push2 map from JSON file provided in Push2's interface doc 62 | # https://github.com/Ableton/push-interface/blob/master/doc/Push2-map.json 63 | self.push2_map = push2_map 64 | 65 | # Init individual sections 66 | self.display = Push2Display(self) 67 | self.pads = Push2Pads(self) 68 | self.buttons = Push2Buttons(self) 69 | self.encoders = Push2Encoders(self) 70 | self.touchstrip = Push2TouchStrip(self) 71 | 72 | # Initialize MIDI IN connection with push 73 | self.configure_midi(skip_midi_out=True) 74 | 75 | # NOTE: no need to initialize MIDI out connection and connection with display because 76 | # these will be lazily initialized when required (i.e., when attempting to send MIDI to push 77 | # or attempting to use the display) 78 | 79 | # Start thread that will continuously check whether the last "active sensing" MIDI message from 80 | # push was received. If active sensing messages stop, will set midi ports to None and trigger 81 | # a "midi disconnected" action 82 | def check_active_sensing(f_stop): 83 | if self.last_active_sensing_received is not None: 84 | if time.time() - self.last_active_sensing_received > PUSH2_MIDI_ACTIVE_SENSING_MAX_INTERVAL: 85 | ''' 86 | # Don't set midi port connections to None because if these were ever initialized, will remain 87 | # active once Push2 MIDI comes back (e.g. after Push2 reset) and we will start receiving again 88 | # active sensing messages (and will be able to re-trigger "push midi connected" message without 89 | # actively continuously checking for MIDI connection using some sort of polling strategy. 90 | if self.midi_is_configured(): 91 | if self.midi_in_port is not None: 92 | self.midi_in_port.close() 93 | self.midi_in_port = None 94 | if self.midi_out_port is not None: 95 | self.midi_out_port.close() 96 | self.midi_out_port = None 97 | ''' 98 | self.trigger_action(ACTION_MIDI_DISCONNECTED) 99 | self.last_active_sensing_received = None 100 | if not f_stop.is_set(): 101 | threading.Timer(0.3, check_active_sensing, [f_stop]).start() # Run this check every 300 ms 102 | 103 | self.f_stop = threading.Event() 104 | check_active_sensing(self.f_stop) 105 | 106 | # Initialize simulator (if requested) 107 | if run_simulator: 108 | self.simulator_controller = start_simulator(self, port=simulator_port, use_virtual_midi_out=simulator_use_virtual_midi_out) 109 | 110 | 111 | def stop_active_sensing_thread(self): 112 | self.f_stop.set() 113 | 114 | 115 | def set_push2_reconnect_call_interval(self, new_interval): 116 | self.function_call_interval_limit_overwrite = new_interval 117 | self.display.function_call_interval_limit_overwrite = new_interval 118 | 119 | 120 | def trigger_action(self, *args, **kwargs): 121 | action_name = args[0] 122 | new_args = [self] 123 | if len(args) > 1: 124 | new_args += list(args[1:]) 125 | for action, func in action_handler_registry.items(): 126 | if action == action_name: 127 | func[0](*new_args, **kwargs) # TODO: why is func a 1-element list? 128 | 129 | 130 | @function_call_interval_limit(PUSH2_RECONNECT_INTERVAL) 131 | def configure_midi(self, skip_midi_out=False, skip_midi_in=False): 132 | """Calling this function will try to configure MIDI in/out connection with Push2. If configuration 133 | is already properly set up, nothing will be done so it is safe to call this even if MIDI has already 134 | been configured. 135 | 136 | This function is decorated with 'function_call_interval_limit' which means that it is only going to be executed 137 | if PUSH2_RECONNECT_INTERVAL seconds have passed since the last time the function was called. This is to avoid 138 | potential problems trying to configure MIDI many times per second. To ignore this limitation, 'self.configure_midi_out()' 139 | and 'self.configure_midi_in()' can be called instead. 140 | """ 141 | if not skip_midi_out: 142 | try: 143 | self.configure_midi_out() 144 | except (Push2MIDIeviceNotFound, ) as e: 145 | log_error = False 146 | if self.simulator_controller is not None: 147 | if not hasattr(self, 'midi_out_init_error_shown'): 148 | log_error = True 149 | self.midi_out_init_error_shown = True 150 | else: 151 | log_error = True 152 | if log_error: 153 | logging.error('Could not initialize Push 2 MIDI out: {0}'.format(e)) 154 | 155 | if not skip_midi_in: 156 | try: 157 | self.configure_midi_in() 158 | except (Push2MIDIeviceNotFound, ) as e: 159 | log_error = False 160 | if self.simulator_controller is not None: 161 | if not hasattr(self, 'midi_in_init_error_shown'): 162 | log_error = True 163 | self.midi_in_init_error_shown = True 164 | else: 165 | log_error = True 166 | if log_error: 167 | logging.error('Could not initialize Push 2 MIDI in: {0}'.format(e)) 168 | 169 | 170 | def configure_midi_in(self): 171 | if self.midi_in_port is None: 172 | port_name_to_use = None 173 | for port_name in mido.get_input_names(): 174 | if is_push_midi_in_port_name(port_name, use_user_port=self.use_user_midi_port): 175 | port_name_to_use = port_name 176 | break 177 | 178 | if port_name_to_use is None: 179 | raise Push2MIDIeviceNotFound 180 | 181 | try: 182 | self.midi_in_port = mido.open_input(port_name_to_use) 183 | # Disable Active Sense message filtering so we can receive those messages comming from Push and 184 | # detect if Push MIDI gets disconnected 185 | self.midi_in_port._rt.ignore_types(False, False, False) 186 | self.midi_in_port.callback = self.on_midi_message 187 | except OSError as e: 188 | raise Push2MIDIeviceNotFound 189 | 190 | 191 | def configure_midi_out(self): 192 | if self.midi_out_port is None: 193 | port_name_to_use = None 194 | for port_name in mido.get_output_names(): 195 | if is_push_midi_out_port_name(port_name, use_user_port=self.use_user_midi_port): 196 | port_name_to_use = port_name 197 | break 198 | 199 | if port_name_to_use is None: 200 | raise Push2MIDIeviceNotFound 201 | 202 | try: 203 | self.midi_out_port = mido.open_output(port_name_to_use) 204 | except OSError as e: 205 | raise Push2MIDIeviceNotFound 206 | 207 | 208 | def midi_is_configured(self): 209 | """Returns True if MIDI communication with Push2 is properly configured, False otherwise 210 | """ 211 | return self.midi_in_port is not None and self.midi_out_port is not None 212 | 213 | 214 | def send_midi_to_push(self, msg): 215 | 216 | # If MIDI is not configured, configure it now 217 | if not self.midi_is_configured(): 218 | self.configure_midi() 219 | 220 | # If MIDI out was properly configured, send the MIDI message 221 | if self.midi_out_port is not None: 222 | self.midi_out_port.send(msg) 223 | 224 | 225 | def on_midi_message(self, message): 226 | """Handle incomming MIDI messages from Push. 227 | Call `on_midi_nessage` for each individual section. 228 | """ 229 | current_time = time.time() 230 | if (message.type == "active_sensing"): 231 | active_sensing_was_none = self.last_active_sensing_received is None 232 | self.last_active_sensing_received = current_time 233 | if active_sensing_was_none: 234 | # Means this is first active_sensing received message (possibly after Push2 restart) and therefore initial MIDI setup (if any) should be done 235 | self.pads.reset_current_pads_state() # Reset stored pads state (if any) to avoid messages not being sent because of state 236 | self.trigger_action(ACTION_MIDI_CONNECTED) 237 | self.last_action_midi_connection_action_triggered = current_time 238 | else: 239 | if self.last_active_sensing_received is not None and current_time - self.last_action_midi_connection_action_triggered > 1: 240 | # Right after first "active sensing" message is received (which means MIDI IN conneciton with Push is properly set), 241 | # ignore the next 1 second of MIDI in messages as for some reason these include a burst of messages from Push which we 242 | # are not interested in (probably some internal state which Ableton uses but we don't care about?) 243 | 244 | # Send received message to each "part" so it is processes accordingly 245 | for func in [self.pads.on_midi_message, self.buttons.on_midi_message, self.encoders.on_midi_message, self.touchstrip.on_midi_message]: 246 | action_taken = func(message) 247 | if action_taken: 248 | break # Early return from for loop to avoid running unnecessary checks 249 | 250 | # Also check for some other extra general message types here 251 | if message.type == MIDO_CONTROLCHANGE: 252 | if message.control == 64: # Sustain pedal 253 | self.trigger_action(ACTION_SUSTAIN_PEDAL, message.value >= 64) 254 | 255 | logging.debug('Received MIDI message from Push: {0}'.format(message)) 256 | 257 | 258 | def set_color_palette_entry(self, color_idx, color_name, rgb=None, bw=None, allow_overwrite=False): 259 | """Updates internal Push color palette so that colors for pads and buttons can be customized. 260 | Using this method will update the color palette in Push hardware, and also the color palette used by the Push2 python object 261 | so that colors can be changed accordingly. 262 | 263 | The way color palette is updated is by specifying a color index ('color_idx' parameter, range [0..127]) and its corresponding rgb values ('rgb' 264 | parameter, as a 3-element list of floats from [0..1] or integers [0..255]) and/or black and white value ('bw' parameter, as a single 265 | brilliance float from [0..1] or integer [0..255]). Therefore, this method allows you to specify a color for the same color entry in 266 | the RGB and BW palettes. If either 'rgb' or 'bw' is not specified, this method will make an "intelligent" guess to set the other. In 267 | addition to 'color_idx' and 'rgb'/'bw' values, a 'color_name' must be given to further identify the color. 'color_name' should be 268 | either a str (in this case the same name will be used for the RGB and BW palettes), or as a 2-element list with the color corresponding 269 | to the 'rgb' color (1st element) and then name for the 'bw' color (2nd element). 270 | 271 | If 'allow_overwrite' is not set to True, this method will raise exception if the given color name already exists in the RGB or BW color 272 | palette. 273 | 274 | Note that changes in the Push color palete using this method won't become active until method 'reapply_color_palette' is called. 275 | 276 | Examples: 277 | 278 | # Configure palette entry 0 to be red (for rgb colors) and gray (for bw colors) 279 | set_color_palette_entry(0, ['red', 'gray'], rgb=[255, 0, 0], bw=128) 280 | 281 | # Configure palette entry 0 to be dark green (for rgb colors) and white (for bw colors) 282 | set_color_palette_entry(0, ['dark green', 'white'], rgb=[0, 100, 0], bw=255) 283 | 284 | # Apply the changes in Push 285 | reapply_color_palette() 286 | 287 | 288 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#262-rgb-led-color-processing 289 | """ 290 | 291 | assert type(color_idx) == int, 'Parameter "color_idx" must be an integer' 292 | assert 0 <= color_idx <= 127, 'Parameter "color_idx" must be in range [0..127]' 293 | 294 | assert rgb is not None or bw is not None, 'At least "rgb" or "bw" parameter (or both) should be provided' 295 | 296 | if rgb is not None: 297 | assert len(rgb) == 3, 'Parameter "rgb" should have 3 elements' 298 | 299 | if type(color_name) == str: 300 | color_names = [color_name, color_name] 301 | else: 302 | assert len(color_name) == 2, 'Parameter "color_name" should have 2 elements' 303 | color_names = color_name 304 | 305 | if not allow_overwrite: 306 | assert color_names[0] not in [rgb_color_name for rgb_color_name, _ in self.color_palette.values()], 'A color with name "{0}" for RGB palette already exists'.format(color_names[0]) 307 | assert color_names[1] not in [bw_color_name for _, bw_color_name in self.color_palette.values()], 'A color with name "{0}" for BW palette already exists'.format(color_names[1]) 308 | 309 | def check_color_range(c): 310 | # If color is float, map it to [0..255], also check range is inside [0..255] 311 | if type(c) == float: 312 | c = int(round(c * 255)) 313 | if c < 0: 314 | c = 0 315 | elif c > 255: 316 | c = 255 317 | return c 318 | 319 | if rgb is not None: 320 | r = check_color_range(rgb[0]) 321 | g = check_color_range(rgb[1]) 322 | b = check_color_range(rgb[2]) 323 | else: 324 | # If rgb is not provided, w will have been provided, use this number for all components 325 | r = check_color_range(bw) 326 | g = r 327 | b = r 328 | 329 | if bw is not None: 330 | w = check_color_range(bw) 331 | else: 332 | # If white is not provided, rgb will have been provided, use an average of it ti decide white 333 | w = check_color_range(int((r + g + b) / 3)) 334 | 335 | # Send message to Push to update internal color palette 336 | red_bytes = [r % 128, r // 128] 337 | green_bytes = [g % 128, g // 128] 338 | blue_bytes = [b % 128, b // 128] 339 | white_bytes = [w % 128, w // 128] 340 | message_bytes = PUSH2_SYSEX_PREFACE_BYTES + [0x03] + [color_idx] + red_bytes + green_bytes + blue_bytes + white_bytes + PUSH2_SYSEX_END_BYTES 341 | msg = mido.Message.from_bytes(message_bytes) 342 | self.send_midi_to_push(msg) 343 | 344 | # Update self.color_palette with given color names (first one for rgb, second one for bw) 345 | self.color_palette[color_idx] = color_names 346 | 347 | # Update color in simulator (if it is being run...) 348 | if self.simulator_controller is not None: 349 | self.simulator_controller.update_color_palette_entry(color_idx, (r, g, b), (w, w, w)) 350 | 351 | 352 | def update_rgb_color_palette_entry(self, color_name, rgb): 353 | """This method finds an RGB color name in the RGB palette and updates it's color values to the given rgb. 354 | See 'set_color_palette_entry' for details on how rgb values should be given. 355 | Note that if ther's a BW color in the same RGB color name entry, it will be overwriten. 356 | Note that changes in the Push color palete using this method won't become active until method 'reapply_color_palette' is called. 357 | 358 | Example: 359 | 360 | # Customize 'green' colour 361 | update_rgb_color_palette_entry('green', [0, 0, 240]) 362 | 363 | # Apply the changes in Push 364 | reapply_color_palette() 365 | """ 366 | idx = None 367 | for color_idx, (rgb_color_name, _) in self.color_palette.items(): 368 | if color_name == rgb_color_name: 369 | idx = color_idx 370 | assert idx is not None, 'No color with name {0} is in RGB color palette'.format(color_name) 371 | self.set_color_palette_entry(idx, color_name, rgb=rgb, allow_overwrite=True) 372 | 373 | 374 | def get_rgb_color(self, color_name): 375 | """Get correpsonding color index of the color palette for a RGB color name. 376 | If color is not found, the default RGB index value will be returned. 377 | """ 378 | for color_idx, (rgb_color_name, _) in self.color_palette.items(): 379 | if color_name == rgb_color_name: 380 | return color_idx 381 | return DEFAULT_RGB_COLOR 382 | 383 | 384 | def get_bw_color(self, color_name): 385 | """Get correpsonding color index of the color palette for a BW color name. 386 | If color is not found, the default BW index value will be returned. 387 | """ 388 | for color_idx, (_, bw_color_name) in self.color_palette.items(): 389 | if color_name == bw_color_name: 390 | return color_idx 391 | return DEFAULT_BW_COLOR 392 | 393 | def reapply_color_palette(self): 394 | """This method sends a sysex message to Push to make it update the colors of all pads and buttons according to the color palette entries 395 | that have been updated using the 'set_color_palette_entry' method. 396 | See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#262-rgb-led-color-processing 397 | """ 398 | message_bytes = PUSH2_SYSEX_PREFACE_BYTES + [0x05] + PUSH2_SYSEX_END_BYTES 399 | msg = mido.Message.from_bytes(message_bytes) 400 | self.send_midi_to_push(msg) 401 | 402 | def display_is_configured(self): 403 | """Returns True if communication with Push2 display is properly configured, False otherwise 404 | """ 405 | return self.display is not None and self.display.usb_endpoint is not None 406 | 407 | 408 | def action_handler(action_name, button_name=None, pad_n=None, pad_ij=None, encoder_name=None): 409 | """ 410 | Generic action handler decorator used by other specific decorators. 411 | This decorator should not be used directly. Specific decorators for individual actions should be used instead. 412 | """ 413 | def wrapper(func): 414 | action = action_name 415 | if action_name in [ACTION_BUTTON_PRESSED, ACTION_BUTTON_RELEASED] and button_name is not None: 416 | # If the action is pressing or releasing a button and a spcific button name is given, 417 | # include the button name in the action name so that it can be triggered individually 418 | action = get_individual_button_action_name(action_name, button_name) 419 | if action_name in [ACTION_PAD_PRESSED, ACTION_PAD_RELEASED, ACTION_PAD_AFTERTOUCH] and (pad_n is not None or pad_ij is not None): 420 | # If the action is pressing, releasing or aftertouching a pad and a spcific pad number or 421 | # pad ij coordinates are given name was given, include the pad name or cordinate in the 422 | # action name so that it can be triggered individually 423 | action = get_individual_pad_action_name(action_name, pad_n=pad_n, pad_ij=pad_ij) 424 | if action_name in [ACTION_ENCODER_ROTATED, ACTION_ENCODER_TOUCHED, ACTION_ENCODER_RELEASED] and encoder_name is not None: 425 | # If the action is rotating, touching or releasing a rotatory encoder and a specific encoder 426 | # name is given, include the encoder name in the action name so that it can be triggered individually 427 | action = get_individual_encoder_action_name(action_name, encoder_name=encoder_name) 428 | logging.debug('Registered handler {0} for action {1}'.format(func, action)) 429 | action_handler_registry[action].append(func) 430 | return func 431 | return wrapper 432 | 433 | 434 | def on_button_pressed(button_name=None): 435 | """Shortcut for registering handlers for ACTION_BUTTON_PRESSED events. 436 | Optional "button_name" argument is to link the handler to a specific button. 437 | Functions decorated with this decorator will be called with the following positional 438 | arguments: 439 | * Push2 object instance 440 | * Button name (string, only if button name not specified in decorator) 441 | 442 | Examples: 443 | 444 | @push2_python.on_button_pressed() 445 | def function(push, button_name): 446 | print('Button', button_name, 'pressed') 447 | 448 | @push2_python.on_button_pressed(push2_python.constants.BUTTON_1_16) 449 | def function(push): 450 | print('Button 1/16 pressed') 451 | """ 452 | return action_handler(ACTION_BUTTON_PRESSED, button_name=button_name) 453 | 454 | 455 | def on_button_released(button_name=None): 456 | """Shortcut for registering handlers for ACTION_BUTTON_RELEASED events. 457 | Optional "button_name" argument is to link the handler to a specific button. 458 | Functions decorated with this decorator will be called with the following positional 459 | arguments: 460 | * Push2 object instance 461 | * Button name (string, only if button name not specified in decorator) 462 | 463 | Examples: 464 | 465 | @push2_python.on_button_released() 466 | def function(push, button_name): 467 | print('Button', button_name, 'released') 468 | 469 | @push2_python.on_button_released(push2_python.constants.BUTTON_1_16) 470 | def function(push): 471 | print('Button 1/6 released') 472 | """ 473 | return action_handler(ACTION_BUTTON_RELEASED, button_name=button_name) 474 | 475 | 476 | def on_touchstrip(): 477 | """Shortcut for registering handlers for ACTION_TOUCHSTRIP_TOUCHED events. Push2's 478 | touchstrip can be configured to work eithher as a pitch bend "wheel" (the default) 479 | or as a modualtion "wheel". When configured as pitch bend, the touchstrip values received 480 | in this method (see below) will correspond to pitch bend values [-8192, 8128]. If configured as 481 | modulation wheel, this function will receive the value of the modulation [0, 127]. Both 482 | modes can be configured by calling "set_modulation_wheel_mode" or "set_pitch_bend_mode" methods 483 | in Push2.touchstrip. 484 | Functions decorated with this decorator will be called with the following positional 485 | arguments: 486 | * Push2 object instance 487 | * Touchstrip value (int) 488 | 489 | Examples: 490 | 491 | @push2_python.on_touchstrip() 492 | def function(push, value): 493 | print('Touchstrip touched with value', value) 494 | """ 495 | return action_handler(ACTION_TOUCHSTRIP_TOUCHED) 496 | 497 | 498 | def on_pad_pressed(pad_n=None, pad_ij=None): 499 | """Shortcut for registering handlers for ACTION_PAD_PRESSED events. 500 | Optional "pad_n" or "pad_ij" arguments are to link the handler to a specific pad. 501 | Functions decorated with this decorator will be called with the following positional 502 | arguments: 503 | * Push2 object instance 504 | * Pad number (int, only if pad not specified in decorator) 505 | * Pad ij coordinates (tuple, only if pad not specified in decorator) 506 | * Velocity value (int 0-127) 507 | 508 | Examples: 509 | 510 | @push2_python.on_pad_pressed() 511 | def function(push, pad_n, pad_ij, velocity): 512 | print('Pad', pad_n, 'pressed with velocity', velocity) 513 | 514 | @push2_python.on_pad_pressed(pad_n=36) 515 | def function(push, velocity): 516 | print('Pad 36 pressed with velocity', velocity) 517 | 518 | @push2_python.on_pad_pressed(pad_ij=(0,3)) 519 | def function(push, velocity): 520 | print('Pad (0, 3) pressed with velocity', velocity) 521 | """ 522 | return action_handler(ACTION_PAD_PRESSED, pad_n=pad_n, pad_ij=pad_ij) 523 | 524 | 525 | def on_pad_released(pad_n=None, pad_ij=None): 526 | """Shortcut for registering handlers for ACTION_PAD_RELEASED events. 527 | Optional "pad_n" or "pad_ij" arguments are to link the handler to a specific pad. 528 | Functions decorated with this decorator will be called with the following positional 529 | arguments: 530 | * Push2 object instance 531 | * Pad number (int, only if pad not specified in decorator) 532 | * Pad ij coordinates (tuple, only if pad not specified in decorator) 533 | * Release velocity value (int 0-127) 534 | 535 | Examples: 536 | 537 | @push2_python.on_pad_released() 538 | def function(push, pad_n, pad_ij, velocity): 539 | print('Pad', pad_n, 'released with velocity', velocity) 540 | 541 | @push2_python.on_pad_released(pad_n=36) 542 | def function(push, velocity): 543 | print('Pad 36 released with velocity', velocity) 544 | 545 | @push2_python.on_pad_released(pad_ij=(0,3)) 546 | def function(push, velocity): 547 | print('Pad (0, 3) released with velocity', velocity) 548 | """ 549 | return action_handler(ACTION_PAD_RELEASED, pad_n=pad_n, pad_ij=pad_ij) 550 | 551 | 552 | def on_pad_aftertouch(pad_n=None, pad_ij=None): 553 | """Shortcut for registering handlers for ACTION_PAD_AFTERTOUCH events. This can 554 | work in "channel aftertouch" (cAT) or "polyphonic aftertouch" (polyAT) modes, which 555 | are configured using "set_polyphonic_aftertouch" or "set_channel_aftertouch" methods 556 | in Push2.pads. cAT mode is enabled by default. In polyAT mode Push will send individual 557 | aftertouch data for each pad, while in cAT mode aftertouch value will be shared for 558 | all pads. In polyAT mode, optional "pad_n" or "pad_ij" arguments can be passed to the 559 | decorator to link the handler to a specific pad adftertouch action. 560 | Functions decorated with this decorator will be called with the following positional 561 | arguments: 562 | * Push2 object instance 563 | * Pad number (int, only if pad not specified in decorator, will be none in cAT mode) 564 | * Pad ij position (tuple, only if pad not specified in decorator, will be none in cAT mode) 565 | * Aftertouch value (int 0-127) 566 | 567 | Examples: 568 | 569 | @push2_python.on_pad_aftertouch() 570 | def function(push, pad_n, pad_ij, value): 571 | if pad_n is not None: 572 | print('Pad', pad_n, 'aftertouch with value', value) 573 | else: 574 | print('Channel aftertouch with value', value) 575 | 576 | @push2_python.on_pad_aftertouch(pad_n=36) 577 | def function(push, value): 578 | print('Pad 36 aftertouched with value', value) 579 | 580 | @push2_python.on_pad_aftertouch(pad_ij=(0,3)) 581 | def function(push, value): 582 | print('Pad (0, 3) aftertouched with value', value) 583 | """ 584 | return action_handler(ACTION_PAD_AFTERTOUCH, pad_n=pad_n, pad_ij=pad_ij) 585 | 586 | 587 | def on_encoder_rotated(encoder_name=None): 588 | """Shortcut for registering handlers for ACTION_ENCODER_ROTATED events. 589 | Optional "encoder_name" argument is to link the handler to a specific encoder. 590 | Functions decorated with this decorator will be called with the following positional 591 | arguments: 592 | * Push2 object instance 593 | * Encoder name (string, only if pad not specified in decorator) 594 | * Encoder increment (int, 1 for clockwise rotation and -1 for counter-clockwise) 595 | 596 | Examples: 597 | 598 | @push2_python.on_encoder_rotated() 599 | def function(push, encoder_name, increment): 600 | print('Encoder', encoder_name, 'rotated with increment', increment) 601 | 602 | @push2_python.on_encoder_rotated(push2_python.constants.ENCODER_TRACK1_ENCODER) 603 | def function(push, increment): 604 | print('Encoder for Track 1 rotated with increment', increment) 605 | """ 606 | return action_handler(ACTION_ENCODER_ROTATED, encoder_name=encoder_name) 607 | 608 | 609 | def on_encoder_touched(encoder_name=None): 610 | """Shortcut for registering handlers for ACTION_ENCODER_TOUCHED events. 611 | Optional "encoder_name" argument is to link the handler to a specific encoder. 612 | Functions decorated with this decorator will be called with the following positional 613 | arguments: 614 | * Push2 object instance 615 | * Encoder name (string, only if pad not specified in decorator) 616 | 617 | Examples: 618 | 619 | @push2_python.on_encoder_touched() 620 | def function(push, encoder_name): 621 | print('Encoder', encoder_name, 'touched') 622 | 623 | @push2_python.on_encoder_touched(push2_python.constants.ENCODER_TRACK1_ENCODER) 624 | def function(push): 625 | print('Encoder for Track 1 touched') 626 | """ 627 | return action_handler(ACTION_ENCODER_TOUCHED, encoder_name=encoder_name) 628 | 629 | 630 | def on_encoder_released(encoder_name=None): 631 | """Shortcut for registering handlers for ACTION_ENCODER_RELEASED events. 632 | Optional "encoder_name" argument is to link the handler to a specific encoder. 633 | Functions decorated with this decorator will be called with the following positional 634 | arguments: 635 | * Push2 object instance 636 | * Encoder name (string, only if pad not specified in decorator) 637 | 638 | Examples: 639 | 640 | @push2_python.on_encoder_released() 641 | def function(push, encoder_name): 642 | print('Encoder', encoder_name, 'released') 643 | 644 | @push2_python.on_encoder_released(push2_python.constants.ENCODER_TRACK1_ENCODER) 645 | def function(push): 646 | print('Encoder for Track 1 released') 647 | """ 648 | return action_handler(ACTION_ENCODER_RELEASED, encoder_name=encoder_name) 649 | 650 | 651 | def on_display_connected(): 652 | """Shortcut for registering handlers for ACTION_DISPLAY_CONNECTED events. 653 | Functions decorated with this decorator will be called when push2-python successfully connects 654 | with the Push2 display and will have the following positional arguments: 655 | * Push2 object instance 656 | 657 | Examples: 658 | 659 | @push2_python.on_display_connected() 660 | def function(push): 661 | print('Display is ready to receive frames') 662 | """ 663 | return action_handler(ACTION_DISPLAY_CONNECTED) 664 | 665 | 666 | def on_display_disconnected(): 667 | """Shortcut for registering handlers for ACTION_DISPLAY_DISCONNECTED events. 668 | Functions decorated with this decorator will be called when push2-python loses connection with the Push2 669 | display. It will have the following positional arguments: 670 | * Push2 object instance 671 | 672 | Examples: 673 | 674 | @push2_python.on_display_disconnected() 675 | def function(push): 676 | print('Connection with Push2 display was just lost!') 677 | """ 678 | return action_handler(ACTION_DISPLAY_DISCONNECTED) 679 | 680 | 681 | def on_midi_connected(): 682 | """Shortcut for registering handlers for ACTION_MIDI_CONNECTED events. 683 | Functions decorated with this decorator will be called when push2-python successfully connects 684 | with Push2 MIDI devices and will have the following positional arguments: 685 | * Push2 object instance 686 | 687 | Examples: 688 | 689 | @push2_python.on_midi_connected() 690 | def function(push): 691 | print('Push is ready to send and receive MIDI messages (set pad colors, buttons, advanced configuration, etc...)') 692 | """ 693 | return action_handler(ACTION_MIDI_CONNECTED) 694 | 695 | 696 | def on_midi_disconnected(): 697 | """Shortcut for registering handlers for ACTION_MIDI_DISCONNECTED events. 698 | Functions decorated with this decorator will be called when push2-python loses MIDI connection with Push2. 699 | It will have the following positional arguments: 700 | * Push2 object instance 701 | 702 | Examples: 703 | 704 | @push2_python.on_midi_disconnected() 705 | def function(push): 706 | print('MIDI connection to push was just lost!') 707 | """ 708 | return action_handler(ACTION_MIDI_DISCONNECTED) 709 | 710 | 711 | def on_sustain_pedal(): 712 | """Shortcut for registering handlers for ACTION_SUSTAIN_PEDAL events. 713 | Functions decorated with this decorator will be called when the sustain pedal connected to Push2 sustain 714 | pedal jack is either pressed or released. It will have the following positional arguments: 715 | * Push2 object instance 716 | * Sustain pedal state (True if sustain pedal was pressed, False if sustain pedal was released) 717 | 718 | Examples: 719 | 720 | @push2_python.on_sustain_pedal() 721 | def function(push, sustain_on): 722 | print('Sustain predal pressed' if sustain_on else 'Sustain predal released') 723 | """ 724 | return action_handler(ACTION_SUSTAIN_PEDAL) 725 | -------------------------------------------------------------------------------- /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/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/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/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/exceptions.py: -------------------------------------------------------------------------------- 1 | class Push2USBDeviceNotFound(Exception): 2 | pass 3 | 4 | class Push2USBDeviceConfigurationError(Exception): 5 | pass 6 | 7 | class Push2MIDIeviceNotFound(Exception): 8 | pass 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /push2_python/push2_map.py: -------------------------------------------------------------------------------- 1 | # This a Python dictionary version of https://github.com/Ableton/push-interface/blob/master/doc/Push2-map.json 2 | 3 | push2_map = { 4 | "Parts": { 5 | "Pads": [ 6 | { 7 | "Number": 36, 8 | "Message": "note", 9 | "Color": True, 10 | "Name": "Pad S8 T1" 11 | }, 12 | { 13 | "Number": 37, 14 | "Message": "note", 15 | "Color": True, 16 | "Name": "Pad S8 T2" 17 | }, 18 | { 19 | "Number": 38, 20 | "Message": "note", 21 | "Color": True, 22 | "Name": "Pad S8 T3" 23 | }, 24 | { 25 | "Number": 39, 26 | "Message": "note", 27 | "Color": True, 28 | "Name": "Pad S8 T4" 29 | }, 30 | { 31 | "Number": 40, 32 | "Message": "note", 33 | "Color": True, 34 | "Name": "Pad S8 T5" 35 | }, 36 | { 37 | "Number": 41, 38 | "Message": "note", 39 | "Color": True, 40 | "Name": "Pad S8 T6" 41 | }, 42 | { 43 | "Number": 42, 44 | "Message": "note", 45 | "Color": True, 46 | "Name": "Pad S8 T7" 47 | }, 48 | { 49 | "Number": 43, 50 | "Message": "note", 51 | "Color": True, 52 | "Name": "Pad S8 T8" 53 | }, 54 | { 55 | "Number": 44, 56 | "Message": "note", 57 | "Color": True, 58 | "Name": "Pad S7 T1" 59 | }, 60 | { 61 | "Number": 45, 62 | "Message": "note", 63 | "Color": True, 64 | "Name": "Pad S7 T2" 65 | }, 66 | { 67 | "Number": 46, 68 | "Message": "note", 69 | "Color": True, 70 | "Name": "Pad S7 T3" 71 | }, 72 | { 73 | "Number": 47, 74 | "Message": "note", 75 | "Color": True, 76 | "Name": "Pad S7 T4" 77 | }, 78 | { 79 | "Number": 48, 80 | "Message": "note", 81 | "Color": True, 82 | "Name": "Pad S7 T5" 83 | }, 84 | { 85 | "Number": 49, 86 | "Message": "note", 87 | "Color": True, 88 | "Name": "Pad S7 T6" 89 | }, 90 | { 91 | "Number": 50, 92 | "Message": "note", 93 | "Color": True, 94 | "Name": "Pad S7 T7" 95 | }, 96 | { 97 | "Number": 51, 98 | "Message": "note", 99 | "Color": True, 100 | "Name": "Pad S7 T8" 101 | }, 102 | { 103 | "Number": 52, 104 | "Message": "note", 105 | "Color": True, 106 | "Name": "Pad S6 T1" 107 | }, 108 | { 109 | "Number": 53, 110 | "Message": "note", 111 | "Color": True, 112 | "Name": "Pad S6 T2" 113 | }, 114 | { 115 | "Number": 54, 116 | "Message": "note", 117 | "Color": True, 118 | "Name": "Pad S6 T3" 119 | }, 120 | { 121 | "Number": 55, 122 | "Message": "note", 123 | "Color": True, 124 | "Name": "Pad S6 T4" 125 | }, 126 | { 127 | "Number": 56, 128 | "Message": "note", 129 | "Color": True, 130 | "Name": "Pad S6 T5" 131 | }, 132 | { 133 | "Number": 57, 134 | "Message": "note", 135 | "Color": True, 136 | "Name": "Pad S6 T6" 137 | }, 138 | { 139 | "Number": 58, 140 | "Message": "note", 141 | "Color": True, 142 | "Name": "Pad S6 T7" 143 | }, 144 | { 145 | "Number": 59, 146 | "Message": "note", 147 | "Color": True, 148 | "Name": "Pad S6 T8" 149 | }, 150 | { 151 | "Number": 60, 152 | "Message": "note", 153 | "Color": True, 154 | "Name": "Pad S5 T1" 155 | }, 156 | { 157 | "Number": 61, 158 | "Message": "note", 159 | "Color": True, 160 | "Name": "Pad S5 T2" 161 | }, 162 | { 163 | "Number": 62, 164 | "Message": "note", 165 | "Color": True, 166 | "Name": "Pad S5 T3" 167 | }, 168 | { 169 | "Number": 63, 170 | "Message": "note", 171 | "Color": True, 172 | "Name": "Pad S5 T4" 173 | }, 174 | { 175 | "Number": 64, 176 | "Message": "note", 177 | "Color": True, 178 | "Name": "Pad S5 T5" 179 | }, 180 | { 181 | "Number": 65, 182 | "Message": "note", 183 | "Color": True, 184 | "Name": "Pad S5 T6" 185 | }, 186 | { 187 | "Number": 66, 188 | "Message": "note", 189 | "Color": True, 190 | "Name": "Pad S5 T7" 191 | }, 192 | { 193 | "Number": 67, 194 | "Message": "note", 195 | "Color": True, 196 | "Name": "Pad S5 T8" 197 | }, 198 | { 199 | "Number": 68, 200 | "Message": "note", 201 | "Color": True, 202 | "Name": "Pad S4 T1" 203 | }, 204 | { 205 | "Number": 69, 206 | "Message": "note", 207 | "Color": True, 208 | "Name": "Pad S4 T2" 209 | }, 210 | { 211 | "Number": 70, 212 | "Message": "note", 213 | "Color": True, 214 | "Name": "Pad S4 T3" 215 | }, 216 | { 217 | "Number": 71, 218 | "Message": "note", 219 | "Color": True, 220 | "Name": "Pad S4 T4" 221 | }, 222 | { 223 | "Number": 72, 224 | "Message": "note", 225 | "Color": True, 226 | "Name": "Pad S4 T5" 227 | }, 228 | { 229 | "Number": 73, 230 | "Message": "note", 231 | "Color": True, 232 | "Name": "Pad S4 T6" 233 | }, 234 | { 235 | "Number": 74, 236 | "Message": "note", 237 | "Color": True, 238 | "Name": "Pad S4 T7" 239 | }, 240 | { 241 | "Number": 75, 242 | "Message": "note", 243 | "Color": True, 244 | "Name": "Pad S4 T8" 245 | }, 246 | { 247 | "Number": 76, 248 | "Message": "note", 249 | "Color": True, 250 | "Name": "Pad S3 T1" 251 | }, 252 | { 253 | "Number": 77, 254 | "Message": "note", 255 | "Color": True, 256 | "Name": "Pad S3 T2" 257 | }, 258 | { 259 | "Number": 78, 260 | "Message": "note", 261 | "Color": True, 262 | "Name": "Pad S3 T3" 263 | }, 264 | { 265 | "Number": 79, 266 | "Message": "note", 267 | "Color": True, 268 | "Name": "Pad S3 T4" 269 | }, 270 | { 271 | "Number": 80, 272 | "Message": "note", 273 | "Color": True, 274 | "Name": "Pad S3 T5" 275 | }, 276 | { 277 | "Number": 81, 278 | "Message": "note", 279 | "Color": True, 280 | "Name": "Pad S3 T6" 281 | }, 282 | { 283 | "Number": 82, 284 | "Message": "note", 285 | "Color": True, 286 | "Name": "Pad S3 T7" 287 | }, 288 | { 289 | "Number": 83, 290 | "Message": "note", 291 | "Color": True, 292 | "Name": "Pad S3 T8" 293 | }, 294 | { 295 | "Number": 84, 296 | "Message": "note", 297 | "Color": True, 298 | "Name": "Pad S2 T1" 299 | }, 300 | { 301 | "Number": 85, 302 | "Message": "note", 303 | "Color": True, 304 | "Name": "Pad S2 T2" 305 | }, 306 | { 307 | "Number": 86, 308 | "Message": "note", 309 | "Color": True, 310 | "Name": "Pad S2 T3" 311 | }, 312 | { 313 | "Number": 87, 314 | "Message": "note", 315 | "Color": True, 316 | "Name": "Pad S2 T4" 317 | }, 318 | { 319 | "Number": 88, 320 | "Message": "note", 321 | "Color": True, 322 | "Name": "Pad S2 T5" 323 | }, 324 | { 325 | "Number": 89, 326 | "Message": "note", 327 | "Color": True, 328 | "Name": "Pad S2 T6" 329 | }, 330 | { 331 | "Number": 90, 332 | "Message": "note", 333 | "Color": True, 334 | "Name": "Pad S2 T7" 335 | }, 336 | { 337 | "Number": 91, 338 | "Message": "note", 339 | "Color": True, 340 | "Name": "Pad S2 T8" 341 | }, 342 | { 343 | "Number": 92, 344 | "Message": "note", 345 | "Color": True, 346 | "Name": "Pad S1 T1" 347 | }, 348 | { 349 | "Number": 93, 350 | "Message": "note", 351 | "Color": True, 352 | "Name": "Pad S1 T2" 353 | }, 354 | { 355 | "Number": 94, 356 | "Message": "note", 357 | "Color": True, 358 | "Name": "Pad S1 T3" 359 | }, 360 | { 361 | "Number": 95, 362 | "Message": "note", 363 | "Color": True, 364 | "Name": "Pad S1 T4" 365 | }, 366 | { 367 | "Number": 96, 368 | "Message": "note", 369 | "Color": True, 370 | "Name": "Pad S1 T5" 371 | }, 372 | { 373 | "Number": 97, 374 | "Message": "note", 375 | "Color": True, 376 | "Name": "Pad S1 T6" 377 | }, 378 | { 379 | "Number": 98, 380 | "Message": "note", 381 | "Color": True, 382 | "Name": "Pad S1 T7" 383 | }, 384 | { 385 | "Number": 99, 386 | "Message": "note", 387 | "Color": True, 388 | "Name": "Pad S1 T8" 389 | } 390 | ], 391 | "Buttons": [ 392 | { 393 | "Number": 3, 394 | "Message": "cc", 395 | "Color": False, 396 | "Name": "Tap Tempo" 397 | }, 398 | { 399 | "Number": 9, 400 | "Message": "cc", 401 | "Color": False, 402 | "Name": "Metronome" 403 | }, 404 | { 405 | "Number": 118, 406 | "Message": "cc", 407 | "Color": False, 408 | "Name": "Delete" 409 | }, 410 | { 411 | "Number": 119, 412 | "Message": "cc", 413 | "Color": False, 414 | "Name": "Undo" 415 | }, 416 | { 417 | "Number": 60, 418 | "Message": "cc", 419 | "Color": True, 420 | "Name": "Mute" 421 | }, 422 | { 423 | "Number": 61, 424 | "Message": "cc", 425 | "Color": True, 426 | "Name": "Solo" 427 | }, 428 | { 429 | "Number": 29, 430 | "Message": "cc", 431 | "Color": True, 432 | "Name": "Stop" 433 | }, 434 | { 435 | "Number": 35, 436 | "Message": "cc", 437 | "Color": False, 438 | "Name": "Convert" 439 | }, 440 | { 441 | "Number": 117, 442 | "Message": "cc", 443 | "Color": False, 444 | "Name": "Double Loop" 445 | }, 446 | { 447 | "Number": 116, 448 | "Message": "cc", 449 | "Color": False, 450 | "Name": "Quantize" 451 | }, 452 | { 453 | "Number": 88, 454 | "Message": "cc", 455 | "Color": False, 456 | "Name": "Duplicate" 457 | }, 458 | { 459 | "Number": 87, 460 | "Message": "cc", 461 | "Color": False, 462 | "Name": "New" 463 | }, 464 | { 465 | "Number": 90, 466 | "Message": "cc", 467 | "Color": False, 468 | "Name": "Fixed Length" 469 | }, 470 | { 471 | "Number": 89, 472 | "Message": "cc", 473 | "Color": True, 474 | "Name": "Automate" 475 | }, 476 | { 477 | "Number": 86, 478 | "Message": "cc", 479 | "Color": True, 480 | "Name": "Record" 481 | }, 482 | { 483 | "Number": 85, 484 | "Message": "cc", 485 | "Color": True, 486 | "Name": "Play" 487 | }, 488 | { 489 | "Number": 102, 490 | "Message": "cc", 491 | "Color": True, 492 | "Name": "Upper Row 1" 493 | }, 494 | { 495 | "Number": 103, 496 | "Message": "cc", 497 | "Color": True, 498 | "Name": "Upper Row 2" 499 | }, 500 | { 501 | "Number": 104, 502 | "Message": "cc", 503 | "Color": True, 504 | "Name": "Upper Row 3" 505 | }, 506 | { 507 | "Number": 105, 508 | "Message": "cc", 509 | "Color": True, 510 | "Name": "Upper Row 4" 511 | }, 512 | { 513 | "Number": 106, 514 | "Message": "cc", 515 | "Color": True, 516 | "Name": "Upper Row 5" 517 | }, 518 | { 519 | "Number": 107, 520 | "Message": "cc", 521 | "Color": True, 522 | "Name": "Upper Row 6" 523 | }, 524 | { 525 | "Number": 108, 526 | "Message": "cc", 527 | "Color": True, 528 | "Name": "Upper Row 7" 529 | }, 530 | { 531 | "Number": 109, 532 | "Message": "cc", 533 | "Color": True, 534 | "Name": "Upper Row 8" 535 | }, 536 | { 537 | "Number": 20, 538 | "Message": "cc", 539 | "Color": True, 540 | "Name": "Lower Row 1" 541 | }, 542 | { 543 | "Number": 21, 544 | "Message": "cc", 545 | "Color": True, 546 | "Name": "Lower Row 2" 547 | }, 548 | { 549 | "Number": 22, 550 | "Message": "cc", 551 | "Color": True, 552 | "Name": "Lower Row 3" 553 | }, 554 | { 555 | "Number": 23, 556 | "Message": "cc", 557 | "Color": True, 558 | "Name": "Lower Row 4" 559 | }, 560 | { 561 | "Number": 24, 562 | "Message": "cc", 563 | "Color": True, 564 | "Name": "Lower Row 5" 565 | }, 566 | { 567 | "Number": 25, 568 | "Message": "cc", 569 | "Color": True, 570 | "Name": "Lower Row 6" 571 | }, 572 | { 573 | "Number": 26, 574 | "Message": "cc", 575 | "Color": True, 576 | "Name": "Lower Row 7" 577 | }, 578 | { 579 | "Number": 27, 580 | "Message": "cc", 581 | "Color": True, 582 | "Name": "Lower Row 8" 583 | }, 584 | { 585 | "Number": 43, 586 | "Message": "cc", 587 | "Color": True, 588 | "Name": "1/32t" 589 | }, 590 | { 591 | "Number": 42, 592 | "Message": "cc", 593 | "Color": True, 594 | "Name": "1/32" 595 | }, 596 | { 597 | "Number": 41, 598 | "Message": "cc", 599 | "Color": True, 600 | "Name": "1/16t" 601 | }, 602 | { 603 | "Number": 40, 604 | "Message": "cc", 605 | "Color": True, 606 | "Name": "1/16" 607 | }, 608 | { 609 | "Number": 39, 610 | "Message": "cc", 611 | "Color": True, 612 | "Name": "1/8t" 613 | }, 614 | { 615 | "Number": 38, 616 | "Message": "cc", 617 | "Color": True, 618 | "Name": "1/8" 619 | }, 620 | { 621 | "Number": 37, 622 | "Message": "cc", 623 | "Color": True, 624 | "Name": "1/4t" 625 | }, 626 | { 627 | "Number": 36, 628 | "Message": "cc", 629 | "Color": True, 630 | "Name": "1/4" 631 | }, 632 | { 633 | "Number": 30, 634 | "Message": "cc", 635 | "Color": False, 636 | "Name": "Setup" 637 | }, 638 | { 639 | "Number": 59, 640 | "Message": "cc", 641 | "Color": False, 642 | "Name": "User" 643 | }, 644 | { 645 | "Number": 52, 646 | "Message": "cc", 647 | "Color": False, 648 | "Name": "Add Device" 649 | }, 650 | { 651 | "Number": 53, 652 | "Message": "cc", 653 | "Color": False, 654 | "Name": "Add Track" 655 | }, 656 | { 657 | "Number": 110, 658 | "Message": "cc", 659 | "Color": False, 660 | "Name": "Device" 661 | }, 662 | { 663 | "Number": 112, 664 | "Message": "cc", 665 | "Color": False, 666 | "Name": "Mix" 667 | }, 668 | { 669 | "Number": 111, 670 | "Message": "cc", 671 | "Color": False, 672 | "Name": "Browse" 673 | }, 674 | { 675 | "Number": 113, 676 | "Message": "cc", 677 | "Color": False, 678 | "Name": "Clip" 679 | }, 680 | { 681 | "Number": 28, 682 | "Message": "cc", 683 | "Color": False, 684 | "Name": "Master" 685 | }, 686 | { 687 | "Number": 46, 688 | "Message": "cc", 689 | "Color": False, 690 | "Name": "Up" 691 | }, 692 | { 693 | "Number": 47, 694 | "Message": "cc", 695 | "Color": False, 696 | "Name": "Down" 697 | }, 698 | { 699 | "Number": 44, 700 | "Message": "cc", 701 | "Color": False, 702 | "Name": "Left" 703 | }, 704 | { 705 | "Number": 45, 706 | "Message": "cc", 707 | "Color": False, 708 | "Name": "Right" 709 | }, 710 | { 711 | "Number": 56, 712 | "Message": "cc", 713 | "Color": False, 714 | "Name": "Repeat" 715 | }, 716 | { 717 | "Number": 57, 718 | "Message": "cc", 719 | "Color": False, 720 | "Name": "Accent" 721 | }, 722 | { 723 | "Number": 58, 724 | "Message": "cc", 725 | "Color": False, 726 | "Name": "Scale" 727 | }, 728 | { 729 | "Number": 31, 730 | "Message": "cc", 731 | "Color": False, 732 | "Name": "Layout" 733 | }, 734 | { 735 | "Number": 50, 736 | "Message": "cc", 737 | "Color": False, 738 | "Name": "Note" 739 | }, 740 | { 741 | "Number": 51, 742 | "Message": "cc", 743 | "Color": False, 744 | "Name": "Session" 745 | }, 746 | { 747 | "Number": 55, 748 | "Message": "cc", 749 | "Color": False, 750 | "Name": "Octave Up" 751 | }, 752 | { 753 | "Number": 54, 754 | "Message": "cc", 755 | "Color": False, 756 | "Name": "Octave Down" 757 | }, 758 | { 759 | "Number": 62, 760 | "Message": "cc", 761 | "Color": False, 762 | "Name": "Page Left" 763 | }, 764 | { 765 | "Number": 63, 766 | "Message": "cc", 767 | "Color": False, 768 | "Name": "Page Right" 769 | }, 770 | { 771 | "Number": 49, 772 | "Message": "cc", 773 | "Color": False, 774 | "Name": "Shift" 775 | }, 776 | { 777 | "Number": 48, 778 | "Message": "cc", 779 | "Color": False, 780 | "Name": "Select" 781 | } 782 | ], 783 | "RotaryEncoders": [ 784 | { 785 | "Number": 14, 786 | "Message": "cc", 787 | "Name": "Tempo Encoder", 788 | "Position": 1, 789 | "Touch": { 790 | "Number": 10, 791 | "Message": "note" 792 | } 793 | }, 794 | { 795 | "Number": 15, 796 | "Message": "cc", 797 | "Name": "Swing Encoder", 798 | "Position": 2, 799 | "Touch": { 800 | "Number": 9, 801 | "Message": "note" 802 | } 803 | }, 804 | { 805 | "Number": 71, 806 | "Message": "cc", 807 | "Name": "Track1 Encoder", 808 | "Position": 3, 809 | "Touch": { 810 | "Number": 0, 811 | "Message": "note" 812 | } 813 | }, 814 | { 815 | "Number": 72, 816 | "Message": "cc", 817 | "Name": "Track2 Encoder", 818 | "Position": 4, 819 | "Touch": { 820 | "Number": 1, 821 | "Message": "note" 822 | } 823 | }, 824 | { 825 | "Number": 73, 826 | "Message": "cc", 827 | "Name": "Track3 Encoder", 828 | "Position": 5, 829 | "Touch": { 830 | "Number": 2, 831 | "Message": "note" 832 | } 833 | }, 834 | { 835 | "Number": 74, 836 | "Message": "cc", 837 | "Name": "Track4 Encoder", 838 | "Position": 6, 839 | "Touch": { 840 | "Number": 3, 841 | "Message": "note" 842 | } 843 | }, 844 | { 845 | "Number": 75, 846 | "Message": "cc", 847 | "Name": "Track5 Encoder", 848 | "Position": 7, 849 | "Touch": { 850 | "Number": 4, 851 | "Message": "note" 852 | } 853 | }, 854 | { 855 | "Number": 76, 856 | "Message": "cc", 857 | "Name": "Track6 Encoder", 858 | "Position": 8, 859 | "Touch": { 860 | "Number": 5, 861 | "Message": "note" 862 | } 863 | }, 864 | { 865 | "Number": 77, 866 | "Message": "cc", 867 | "Name": "Track7 Encoder", 868 | "Position": 9, 869 | "Touch": { 870 | "Number": 6, 871 | "Message": "note" 872 | } 873 | }, 874 | { 875 | "Number": 78, 876 | "Message": "cc", 877 | "Name": "Track8 Encoder", 878 | "Position": 10, 879 | "Touch": { 880 | "Number": 7, 881 | "Message": "note" 882 | } 883 | }, 884 | { 885 | "Number": 79, 886 | "Message": "cc", 887 | "Name": "Master Encoder", 888 | "Position": 11, 889 | "Touch": { 890 | "Number": 8, 891 | "Message": "note" 892 | } 893 | } 894 | ], 895 | "Slider": { 896 | "Touch": { 897 | "Number": 12, 898 | "Message": "pitchbend", 899 | "Name": "Slider" 900 | } 901 | } 902 | }, 903 | "layout": { 904 | "XY": [ 905 | [ 906 | 92, 907 | 93, 908 | 94, 909 | 95, 910 | 96, 911 | 97, 912 | 98, 913 | 99 914 | ], 915 | [ 916 | 84, 917 | 85, 918 | 86, 919 | 87, 920 | 88, 921 | 89, 922 | 90, 923 | 91 924 | ], 925 | [ 926 | 76, 927 | 77, 928 | 78, 929 | 79, 930 | 80, 931 | 81, 932 | 82, 933 | 83 934 | ], 935 | [ 936 | 68, 937 | 69, 938 | 70, 939 | 71, 940 | 72, 941 | 73, 942 | 74, 943 | 75 944 | ], 945 | [ 946 | 60, 947 | 61, 948 | 62, 949 | 63, 950 | 64, 951 | 65, 952 | 66, 953 | 67 954 | ], 955 | [ 956 | 52, 957 | 53, 958 | 54, 959 | 55, 960 | 56, 961 | 57, 962 | 58, 963 | 59 964 | ], 965 | [ 966 | 44, 967 | 45, 968 | 46, 969 | 47, 970 | 48, 971 | 49, 972 | 50, 973 | 51 974 | ], 975 | [ 976 | 36, 977 | 37, 978 | 38, 979 | 39, 980 | 40, 981 | 41, 982 | 42, 983 | 43 984 | ] 985 | ] 986 | } 987 | } 988 | -------------------------------------------------------------------------------- /push2_python/simulator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffont/push2-python/ae91501788469f2c54aa95ed12f37f83505d73f8/push2_python/simulator/__init__.py -------------------------------------------------------------------------------- /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/simulator/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | push2-python simulator 4 | 156 | 157 | 574 | 575 | 576 |

push2-python simulator

577 |

Use shift+click to hold buttons/pads pressed. In encoders, use shift+click in the arrow keys to rotate with bigger increments.

578 |
579 |
580 | 581 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /simulator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ffont/push2-python/ae91501788469f2c54aa95ed12f37f83505d73f8/simulator.png --------------------------------------------------------------------------------