Skip to content

Commit 5453ffd

Browse files
committed
Add support for customization of color palettes
1 parent 950d1d8 commit 5453ffd

File tree

6 files changed

+186
-51
lines changed

6 files changed

+186
-51
lines changed

README.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Utils to interface with [Ableton's Push 2](https://www.ableton.com/en/push/) fro
55
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.
66

77
So far I only implemented some utils to **interface with the display** and some utils for **basic 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+
*EDIT*: customization of color palettes is now implemented!
89

910
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.
1011

@@ -154,14 +155,7 @@ push.buttons.set_button_color(push2_python.constants.BUTTON_PLAY, 'green')
154155
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
155156
[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.
156157

157-
For a list of avilable RGB colors check the keys of the `RGB_COLORS` dictionary in [push2_python/constants.py](https://github.com/ffont/push2-python/blob/master/push2_python/constants.py). Similarly, black and white available colors are defined in the `BLACK_WHITE_COLORS` dictionary in the same file. You can also list available colors in code like this:
158-
159-
```python
160-
print(list(push2_python.constants.RGB_COLORS.keys()))
161-
print(list(push2_python.constants.BLACK_WHITE_COLORS.keys()))
162-
```
163-
164-
**NOTE**: The Push 2 Interface Manual provides a way to configure custom color palettes which has not yet been implemented in `push2-python`. Also, only a limited number of colors is included here but many more are avilable in the default color palette.
158+
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` and `reapply_color_palette` of Push2 object. See the documentation of these methods for more details.
165159

166160

167161
### Interface with the display

push2_python/__init__.py

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
ACTION_BUTTON_RELEASED, ACTION_TOUCHSTRIP_TOUCHED, ACTION_PAD_PRESSED, ACTION_PAD_RELEASED, ACTION_PAD_AFTERTOUCH, \
2020
ACTION_ENCODER_ROTATED, ACTION_ENCODER_TOUCHED, ACTION_ENCODER_RELEASED, PUSH2_RECONNECT_INTERVAL, ACTION_DISPLAY_CONNECTED, \
2121
ACTION_DISPLAY_DISCONNECTED, ACTION_MIDI_CONNECTED, ACTION_MIDI_DISCONNECTED, PUSH2_MIDI_ACTIVE_SENSING_MAX_INTERVAL, ACTION_SUSTAIN_PEDAL, \
22-
MIDO_CONTROLCHANGE
22+
MIDO_CONTROLCHANGE, PUSH2_SYSEX_PREFACE_BYTES, PUSH2_SYSEX_END_BYTES, DEFAULT_COLOR_PALETTE, DEFAULT_RGB_COLOR, DEFAULT_BW_COLOR
2323

2424
logging.basicConfig(stream=sys.stdout, level=logging.ERROR)
2525

@@ -41,6 +41,7 @@ class Push2(object):
4141
use_user_midi_port = False
4242
last_active_sensing_received = None
4343
function_call_interval_limit_overwrite = PUSH2_RECONNECT_INTERVAL
44+
color_palette = DEFAULT_COLOR_PALETTE
4445

4546

4647
def __init__(self, use_user_midi_port=False):
@@ -227,6 +228,146 @@ def on_midi_message(self, message):
227228
logging.debug('Received MIDI message from Push: {0}'.format(message))
228229

229230

231+
def set_color_palette_entry(self, color_idx, color_name, rgb=None, bw=None, allow_overwrite=False):
232+
"""Updates internal Push color palette so that colors for pads and buttons can be customized.
233+
Using this method will update the color palette in Push hardware, and also the color palette used by the Push2 python object
234+
so that colors can be changed accordingly.
235+
236+
The way color palette is updated is by specifying a color index ('color_idx' parameter, range [0..127]) and its corresponding rgb values ('rgb'
237+
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
238+
brilliance float from [0..1] or integer [0..255]). Therefore, this method allows you to specify a color for the same color entry in
239+
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
240+
addition to 'color_idx' and 'rgb'/'bw' values, a 'color_name' must be given to further identify the color. 'color_name' should be
241+
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
242+
to the 'rgb' color (1st element) and then name for the 'bw' color (2nd element).
243+
244+
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
245+
palette.
246+
247+
Note that changes in the Push color palete using this method won't become active until method 'reapply_color_palette' is called.
248+
249+
Examples:
250+
251+
# Configure palette entry 0 to be red (for rgb colors) and gray (for bw colors)
252+
set_color_palette_entry(0, ['red', 'gray'], rgb=[255, 0, 0], bw=128)
253+
254+
# Configure palette entry 0 to be dark green (for rgb colors) and white (for bw colors)
255+
set_color_palette_entry(0, ['dark green', 'white'], rgb=[0, 100, 0], bw=255)
256+
257+
# Apply the changes in Push
258+
reapply_color_palette()
259+
260+
261+
See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#262-rgb-led-color-processing
262+
"""
263+
264+
assert type(color_idx) == int, 'Parameter "color_idx" must be an integer'
265+
assert 0 <= color_idx <= 127, 'Parameter "color_idx" must be in range [0..127]'
266+
267+
assert rgb is not None or bw is not None, 'At least "rgb" or "bw" parameter (or both) should be provided'
268+
269+
if rgb is not None:
270+
assert len(rgb) == 3, 'Parameter "rgb" should have 3 elements'
271+
272+
if type(color_name) == str:
273+
color_names = [color_name, color_name]
274+
else:
275+
assert len(color_name) == 2, 'Parameter "color_name" should have 2 elements'
276+
color_names = color_name
277+
278+
if not allow_overwrite:
279+
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])
280+
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])
281+
282+
def check_color_range(c):
283+
# If color is float, map it to [0..255], also check range is inside [0..255]
284+
if type(c) == float:
285+
c = int(round(c * 255))
286+
if c < 0:
287+
c = 0
288+
elif c > 255:
289+
c = 255
290+
return c
291+
292+
if rgb is not None:
293+
r = check_color_range(rgb[0])
294+
g = check_color_range(rgb[1])
295+
b = check_color_range(rgb[2])
296+
else:
297+
# If rgb is not provided, w will have been provided, use this number for all components
298+
r = check_color_range(bw)
299+
g = r
300+
b = r
301+
302+
if bw is not None:
303+
w = check_color_range(bw)
304+
else:
305+
# If white is not provided, rgb will have been provided, use an average of it ti decide white
306+
w = check_color_range(int((r + g + b) / 3))
307+
308+
# Send message to Push to update internal color palette
309+
red_bytes = [r % 128, r // 128]
310+
green_bytes = [g % 128, g // 128]
311+
blue_bytes = [b % 128, b // 128]
312+
white_bytes = [w % 128, w // 128]
313+
message_bytes = PUSH2_SYSEX_PREFACE_BYTES + [0x03] + [color_idx] + red_bytes + green_bytes + blue_bytes + white_bytes + PUSH2_SYSEX_END_BYTES
314+
msg = mido.Message.from_bytes(message_bytes)
315+
self.send_midi_to_push(msg)
316+
317+
# Update self.color_palette with given color names (first one for rgb, second one for bw)
318+
self.color_palette[color_idx] = color_names
319+
320+
321+
def update_rgb_color_palette_entry(self, color_name, rgb):
322+
"""This method finds an RGB color name in the RGB palette and updates it's color values to the given rgb.
323+
See 'set_color_palette_entry' for details on how rgb values should be given.
324+
Note that if ther's a BW color in the same RGB color name entry, it will be overwriten.
325+
Note that changes in the Push color palete using this method won't become active until method 'reapply_color_palette' is called.
326+
327+
Example:
328+
329+
# Customize 'green' colour
330+
update_rgb_color_palette_entry('green', [0, 0, 240])
331+
332+
# Apply the changes in Push
333+
reapply_color_palette()
334+
"""
335+
idx = None
336+
for color_idx, (rgb_color_name, _) in self.color_palette.items():
337+
if color_name == rgb_color_name:
338+
idx = color_idx
339+
assert idx is not None, 'No color with name {0} is in RGB color palette'.format(color_name)
340+
self.set_color_palette_entry(idx, color_name, rgb=rgb, allow_overwrite=True)
341+
342+
343+
def get_rgb_color(self, color_name):
344+
"""Get correpsonding color index of the color palette for a RGB color name.
345+
If color is not found, the default RGB index value will be returned.
346+
"""
347+
for color_idx, (rgb_color_name, _) in self.color_palette.items():
348+
if color_name == rgb_color_name:
349+
return color_idx
350+
return DEFAULT_RGB_COLOR
351+
352+
353+
def get_bw_color(self, color_name):
354+
"""Get correpsonding color index of the color palette for a BW color name.
355+
If color is not found, the default BW index value will be returned.
356+
"""
357+
for color_idx, (_, bw_color_name) in self.color_palette.items():
358+
if color_name == bw_color_name:
359+
return color_idx
360+
return DEFAULT_BW_COLOR
361+
362+
def reapply_color_palette(self):
363+
"""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
364+
that have been updated using the 'set_color_palette_entry' method.
365+
See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#262-rgb-led-color-processing
366+
"""
367+
message_bytes = PUSH2_SYSEX_PREFACE_BYTES + [0x05] + PUSH2_SYSEX_END_BYTES
368+
msg = mido.Message.from_bytes(message_bytes)
369+
self.send_midi_to_push(msg)
370+
230371
def display_is_configured(self):
231372
"""Returns True if communication with Push2 display is properly configured, False otherwise
232373
"""

push2_python/buttons.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import mido
2-
from .constants import RGB_COLORS, RGB_DEFAULT_COLOR, BLACK_WHITE_COLORS, BLACK_WHITE_DEFAULT_COLOR, ANIMATIONS, \
3-
ANIMATIONS_DEFAULT, MIDO_CONTROLCHANGE, ACTION_BUTTON_PRESSED, ACTION_BUTTON_RELEASED
2+
from .constants import ANIMATIONS, ANIMATIONS_DEFAULT, MIDO_CONTROLCHANGE, ACTION_BUTTON_PRESSED, ACTION_BUTTON_RELEASED
43
from .classes import AbstractPush2Section
54

65

@@ -36,19 +35,19 @@ def button_name_to_button_n(self, button_name):
3635

3736
def set_button_color(self, button_name, color='white', animation='static'):
3837
"""Sets the color of the button with given name.
39-
Color must be an existing key of push2_python.contants.RGB_COLORS dictionary.
40-
NOTE: some buttons only accept black and white colors and color name must be one of push2_python.contants.BLACK_WHITE_COLORS dictionary.
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.
4140
See https://github.com/Ableton/push-interface/blob/master/doc/AbletonPush2MIDIDisplayInterface.asc#setting-led-colors
4241
"""
4342
button_n = self.button_name_to_button_n(button_name)
4443
if button_n is not None:
4544
button = self.button_map[button_n]
4645
if button['Color']:
47-
color = RGB_COLORS.get(color, RGB_DEFAULT_COLOR) # Pick color from RGB palette
46+
color_idx = self.push.get_rgb_color(color)
4847
else:
49-
color = BLACK_WHITE_COLORS.get(color, BLACK_WHITE_DEFAULT_COLOR) # Pick color from black and white palette
48+
color_idx = self.push.get_bw_color(color)
5049
animation = ANIMATIONS.get(animation, ANIMATIONS_DEFAULT)
51-
msg = mido.Message(MIDO_CONTROLCHANGE, control=button_n, value=color, channel=animation)
50+
msg = mido.Message(MIDO_CONTROLCHANGE, control=button_n, value=color_idx, channel=animation)
5251
self.push.send_midi_to_push(msg)
5352

5453
def on_midi_message(self, message):

push2_python/constants.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ def is_push_midi_out_port_name(port_name, use_user_port=False):
6767
MIDI_PITCWHEEL = 'pitchwheel'
6868
MIDO_CONTROLCHANGE = 'control_change'
6969

70+
PUSH2_SYSEX_PREFACE_BYTES = [0xF0, 0x00, 0x21, 0x1D, 0x01, 0x01]
71+
PUSH2_SYSEX_END_BYTES = [0xF7]
72+
7073
# Push 2 Display
7174
DISPLAY_FRAME_HEADER = [0xff, 0xcc, 0xaa, 0x88,
7275
0x00, 0x00, 0x00, 0x00,
@@ -86,34 +89,31 @@ def is_push_midi_out_port_name(port_name, use_user_port=False):
8689
FRAME_FORMAT_RGB565 = 'rgb565'
8790
FRAME_FORMAT_RGB = 'rgb'
8891

89-
# LED colors and anumations
90-
# NOTE: this is a subset of colors from the default palette
91-
# To do this properly we should define a custom color palette and
92-
# send it to Push
93-
RGB_COLORS = {
94-
'black': 0,
95-
'white': 122,
96-
'light_gray': 123,
97-
'dark_gray': 124,
98-
'blue': 125,
99-
'green': 126,
100-
'red': 127,
101-
'orange': 3,
102-
'yellow': 8,
103-
'turquoise': 15,
104-
'purple': 22,
105-
'pink': 25,
106-
}
107-
RGB_DEFAULT_COLOR = 126
108-
109-
BLACK_WHITE_COLORS = {
110-
'black': 0,
111-
'dark_gray': 16,
112-
'light_gray': 48,
113-
'white': 127,
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']
114112
}
115-
BLACK_WHITE_DEFAULT_COLOR = 127
113+
DEFAULT_RGB_COLOR = 126
114+
DEFAULT_BW_COLOR = 127
116115

116+
# Led animations
117117
ANIMATIONS = { # TODO: animations only work when MIDI clock data is sent (?)
118118
'static': 1, # TODO: revise these values, static should be 0?
119119
'blinking': 14

push2_python/encoders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import mido
2-
from .constants import RGB_COLORS, RGB_DEFAULT_COLOR, ANIMATIONS, ANIMATIONS_DEFAULT, MIDO_CONTROLCHANGE, \
2+
from .constants import ANIMATIONS, ANIMATIONS_DEFAULT, MIDO_CONTROLCHANGE, \
33
MIDO_NOTEON, MIDO_NOTEOFF, ACTION_ENCODER_ROTATED, ACTION_ENCODER_TOUCHED, ACTION_ENCODER_RELEASED
44
from .classes import AbstractPush2Section
55

0 commit comments

Comments
 (0)