├── .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
--------------------------------------------------------------------------------