Skip to content

Latest commit

 

History

History
459 lines (336 loc) · 25.4 KB

README.md

File metadata and controls

459 lines (336 loc) · 25.4 KB

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 type uint16. Each uint16 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 type uint16. Each uint16 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