Awesome
push2-python
Utils to interface with Ableton's Push 2 from Python.
These utils follow Ableton's Push 2 MIDI and Display Interface Manual for comunicating with Push 2. I recommend reading Ableton's manual before using this tool.
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 :) UPDATE: customization of color palettes and led animations is now implemented!
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) 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.
push2-python
incorporates a Push2 simulator so you can do development without having your push connected. Check out the simulator section below
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 source source code repository.
Table of Contents
Install
You can install using pip
and pointing at this repository:
pip install git+https://github.com/ffont/push2-python
This will install Python requirements as well. Note however that push2-python
requires pyusb which is based in libusb. You'll most probably need to manually install libusb
for your operative system if pip
does not do it for you.
Documentation
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 and the simple notes given here.
Initializing Push
To interface with Push2 you'll first need to import push2_python
and initialize a Python object as follows:
import push2_python
push = push2_python.Push2()
NOTE: all code snippets below assume you import push2_python
and initialize the Push2
like in the snippet above.
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 and MIDI mode sections of the Push 2 MIDI and Display Interface Manual for more information.
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:
import time
import push2_python
push = push2_python.Push2() # Call this while Push2 is still powered off
while True: # This is your app's main loop
# Try to send some frame to Push2 display to force display connection/reconnection
frame = generate_frame_for_push_display() # Some fake function to do that
push.display.display_frame(frame)
# Try to configure Push2 MIDI at every iteration (if not already configured)
if not push.midi_is_configured():
push.configure_midi()
time.sleep(0.1)
NOTE 1: This calls must be done from your app's main thread (where push2_python.Push2()
is run). Maybe it is possible
to delegate all connection with push2_python
to a different thread (have not tried that), but it is important that all
MIDI configuration calls happen in the same thread because of limitations of the mido
Python MIDI package used by push2_python
.
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.
Setting action handlers for buttons, encoders, pads and the touchstrip
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 the left-most encoder is rotated in this way:
@push2_python.on_encoder_rotated(push2_python.constants.ENCODER_TEMPO_ENCODER)
def on_left_encoder_rotated(push, increment):
print('Left-most encoder rotated with increment', increment)
Similarly, you can set up an action handler that will trigger when play button is pressed in this way:
@push2_python.on_button_pressed(push2_python.constants.BUTTON_PLAY)
def on_play_pressed(push):
print('Play!')
These are all available decorators for setting up action handlers:
@push2_python.on_button_pressed(button_name=None)
@push2_python.on_button_released(button_name=None)
@push2_python.on_touchstrip()
@push2_python.on_pad_pressed(pad_n=None, pad_ij=None)
@push2_python.on_pad_released(pad_n=None, pad_ij=None)
@push2_python.on_pad_aftertouch(pad_n=None, pad_ij=None)
@push2_python.on_encoder_rotated(encoder_name=None)
@push2_python.on_encoder_touched(encoder_name=None)
@push2_python.on_encoder_released(encoder_name=None)
@push2_python.on_display_connected()
@push2_python.on_display_disconnected()
@push2_python.on_midi_connected()
@push2_python.on_midi_disconnected()
@push2_python.on_sustain_pedal()
Full documentation for each of these can be found in their docstrings starting here. Also have a look at the code examples below to get an immediate idea about how it works.
Button names, encoder names, pad numbers and coordinates
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
contents of push2_python/constants.py
or by using the following properties after intializing the Push2
object:
print(push.buttons.available_names)
print(push.encoders.available_names)
Pads are identified either by their number (pad_n
) or by their coordinates (pad_ij
). Pad numbers correspond to the MIDI note numbers assigned
to each pad as defined in Push 2 MIDI and Display Interface Manual (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.
Set pad and button colors
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:
pad_ij = (0, 3) # Fourth pad of the top row
push.pads.set_pad_color(pad_ij, 'green')
You set button colors in a similar way:
push.buttons.set_button_color(push2_python.constants.BUTTON_PLAY, 'green')
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 Push 2 MIDI and Display Interface Manual 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.
For a list of avilable RGB colors check the DEFAULT_COLOR_PALETTE
dictionary in 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.
Set pad and button animations
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:
push.buttons.set_button_color(push2_python.constants.BUTTON_PLAY, 'green', animation=push2_python.constants.ANIMATION_PULSING_QUARTER, animation_end_color='white')
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.
For a list of available animations, check the variables names ANIMATION_*
dictionary in push2_python/constants.py. Also, see the animations section of the Push 2 MIDI and Display Interface Manual for more information about animations.
Adjust pad sensitivity
push2-python
implements methods to adjust Push2 pads sensitivity, in particualr it incorporates methods to adjust the velocity curve (which applies to
note on velocities and to poolyphonic aftertouch sensistivity), and the channel aftertouch range. You can do that using the methods set_channel_aftertouch_range
and set_velocity_curve
from the pads
section. Below are two examples of adjusting sensitivity. Please check methods' documentation for more information.
push.pads.set_channel_aftertouch_range(range_start=401, range_end=800) # Configure channel after touch to be quite sensitive
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
Interface with the display
You interface with Push2's display by senidng frames to be display using the push.display.display_frame
method as follows:
img_frame = ... # Some existing valid img_frame
push.display.display_frame(img_frame, input_format=push2_python.constants.FRAME_FORMAT_BGR565)
img_frame
is expected to by a numpy
array. Depending on the input_format
argument, img_frame
will need to have the following characteristics:
-
for
push2_python.constants.FRAME_FORMAT_BGR565
:numpy
array of shape 910x160 and of typeuint16
. Eachuint16
element specifies rgb color with the following bit position meaning:[b4 b3 b2 b1 b0 g5 g4 g3 g2 g1 g0 r4 r3 r2 r1 r0]
. -
for
push2_python.constants.FRAME_FORMAT_RGB565
:numpy
array of shape 910x160 and of typeuint16
. Eachuint16
element specifies rgb color with the following bit position meaning:[r4 r3 r2 r1 r0 g5 g4 g3 g2 g1 g0 b4 b3 b2 b1 b0]
. -
for
push2_python.constants.FRAME_FORMAT_RGB
: numpy array of shape 910x160x3 with the third dimension representing rgb colors with separate float values for rgb channels (float values in range[0.0, 1.0]
).
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).
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 and here) should give you an idea of how this works. It's easy!
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.
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.
Using the simulator
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:
push = push2_python.Push2(run_simulator=True)
And then, while your app is running, point your browser at localhost:6128
. Here is a screenshot of the simulator in action:
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, 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.
Code examples
Set up action handlers for pads, encoders, buttons and the touchstrip...
import push2_python
# Init Push2
push = push2_python.Push2()
# Now set up some action handlers that will trigger when interacting with Push2
# This is all done using decorators.
@push2_python.on_pad_pressed()
def on_pad_pressed(push, pad_n, pad_ij, velocity):
print('Pad', pad_ij, 'pressed with velocity', velocity)
@push2_python.on_encoder_rotated()
def on_encoder_rotated(push, encoder_name, increment):
print('Encoder', encoder_name, 'rotated', increment)
@push2_python.on_touchstrip()
def on_touchstrip(push, value):
print('Touchstrip touched with value', value)
# You can also set handlers for specic encoders or buttons by passing argument to the decorator
@push2_python.on_encoder_rotated(push2_python.constants.ENCODER_TRACK1_ENCODER)
def on_encoder1_rotated(push, incrememnt):
print('Encoder for Track 1 rotated with increment', increment)
@push2_python.on_button_pressed(push2_python.constants.BUTTON_1_16)
def on_button_pressed(push):
print('Button 1/16 pressed')
# Now start infinite loop so the app keeps running
print('App runnnig...')
while True:
pass
Light up buttons and pads
import push2_python
# Init Push2
push = push2_python.Push2()
# Start by setting all pad colors to white
push.pads.set_all_pads_to_color('white')
@push2_python.on_button_pressed()
def on_button_pressed(push, button_name):
# Set pressed button color to white
push.buttons.set_button_color(button_name, 'white')
@push2_python.on_button_released()
def on_button_released(push, button_name):
# Set released button color to black (off)
push.buttons.set_button_color(button_name, 'black')
@push2_python.on_pad_pressed()
def on_pad_pressed(push, pad_n, pad_ij, velocity):
# Set pressed pad color to green
push.pads.set_pad_color(pad_ij, 'green')
@push2_python.on_pad_released()
def on_pad_released(push, pad_n, pad_ij, velocity):
# Set released pad color back to white
push.pads.set_pad_color(pad_ij, 'white')
# Start infinite loop so the app keeps running
print('App runnnig...')
while True:
pass
Interface with the display (static content)
Here you have some example code for interfacing with Push2's display. Note that this code example requires pillow
Python package, install it with pip install pillow
.
import push2_python
import random
import numpy
from PIL import Image
# Init Push2
push = push2_python.Push2()
# Define util function to generate a frame with some colors to be shown in the display
# Frames are created as matrices of shape 960x160 and with colors defined in bgr565 format
# This function is defined in a rather silly way, could probably be optimized a lot ;)
def generate_3_color_frame():
colors = ['{b:05b}{g:06b}{r:05b}'.format(
r=int(31*random.random()), g=int(63*random.random()), b=int(31*random.random())),
'{b:05b}{g:06b}{r:05b}'.format(
r=int(31*random.random()), g=int(63*random.random()), b=int(31*random.random())),
'{b:05b}{g:06b}{r:05b}'.format(
r=int(31*random.random()), g=int(63*random.random()), b=int(31*random.random()))]
colors = [int(c, 2) for c in colors]
line_bytes = []
for i in range(0, 960): # 960 pixels per line
if i <= 960 // 3:
line_bytes.append(colors[0])
elif 960 // 3 < i <= 2 * 960 // 3:
line_bytes.append(colors[1])
else:
line_bytes.append(colors[2])
frame = []
for i in range(0, 160): # 160 lines
frame.append(line_bytes)
return numpy.array(frame, dtype=numpy.uint16).transpose()
# Pre-generate different color frames
color_frames = list()
for i in range(0, 20):
color_frames.append(generate_3_color_frame())
# Now crate an extra frame which loads an image from a file. Image must be 960x160 pixels.
img = Image.open('test_img_960x160.png')
frame = numpy.array(img)
frame = frame/255 # Convert rgb values to [0.0, 1.0] floats
# Now lets configure some action handlers which will display frames in Push2's display in
# reaction to pad and button presses
@push2_python.on_pad_pressed()
def on_pad_pressed(push, pad_n, pad_ij, velocity):
# Display one of the three color frames on the display
random_frame = random.choice(color_frames)
push.display.display_frame(random_frame)
@push2_python.on_button_pressed()
def on_button_pressed(push, button_name):
# Display the frame with the loaded image
push.display.display_frame(frame, input_format=push2_python.constants.FRAME_FORMAT_RGB)
# Start infinite loop so the app keeps running
print('App runnnig...')
while True:
pass
Interface with the display (dynamic content)
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
Python package, install it with pip install pycairo
(you'll most probably also need to install cairo
before that, see this page for info on that).
import push2_python
import cairo
import numpy
import random
import time
# Init Push2
push = push2_python.Push2()
# Init dictionary to store the state of encoders
encoders_state = dict()
max_encoder_value = 100
for encoder_name in push.encoders.available_names:
encoders_state[encoder_name] = {
'value': int(random.random() * max_encoder_value),
'color': [random.random(), random.random(), random.random()],
}
last_selected_encoder = list(encoders_state.keys())[0]
# Function that generates the contents of the frame do be displayed
def generate_display_frame(encoder_value, encoder_color, encoder_name):
# Prepare cairo canvas
WIDTH, HEIGHT = push2_python.constants.DISPLAY_LINE_PIXELS, push2_python.constants.DISPLAY_N_LINES
surface = cairo.ImageSurface(cairo.FORMAT_RGB16_565, WIDTH, HEIGHT)
ctx = cairo.Context(surface)
# Draw rectangle with width proportional to encoders' value
ctx.set_source_rgb(*encoder_color)
ctx.rectangle(0, 0, WIDTH * (encoder_value/max_encoder_value), HEIGHT)
ctx.fill()
# Add text with encoder name and value
ctx.set_source_rgb(1, 1, 1)
font_size = HEIGHT//3
ctx.set_font_size(font_size)
ctx.select_font_face("Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
ctx.move_to(10, font_size * 2)
ctx.show_text("{0}: {1}".format(encoder_name, encoder_value))
# Turn canvas into numpy array compatible with push.display.display_frame method
buf = surface.get_data()
frame = numpy.ndarray(shape=(HEIGHT, WIDTH), dtype=numpy.uint16, buffer=buf)
frame = frame.transpose()
return frame
# Set up action handlers to react to encoder touches and rotation
@push2_python.on_encoder_rotated()
def on_encoder_rotated(push, encoder_name, increment):
def update_encoder_value(encoder_idx, increment):
updated_value = int(encoders_state[encoder_idx]['value'] + increment)
if updated_value < 0:
encoders_state[encoder_idx]['value'] = 0
elif updated_value > max_encoder_value:
encoders_state[encoder_idx]['value'] = max_encoder_value
else:
encoders_state[encoder_idx]['value'] = updated_value
update_encoder_value(encoder_name, increment)
global last_selected_encoder
last_selected_encoder = encoder_name
@push2_python.on_encoder_touched()
def on_encoder_touched(push, encoder_name):
global last_selected_encoder
last_selected_encoder = encoder_name
# Draw method that will generate the frame to be shown on the display
def draw():
encoder_value = encoders_state[last_selected_encoder]['value']
encoder_color = encoders_state[last_selected_encoder]['color']
frame = generate_display_frame(encoder_value, encoder_color, last_selected_encoder)
push.display.display_frame(frame, input_format=push2_python.constants.FRAME_FORMAT_RGB565)
# Now start infinite loop so the app keeps running
print('App runnnig...')
while True:
draw()
time.sleep(1.0/30) # Sart drawing loop, aim at ~30fps