Apparently I need a longer delay to initialize the pads. Also I put a call to the _init_pads_colors() function at the end of _on_identity_response().
Now the pads colors are initialized correctly (half orange and half red, green when pressed). I have also added a beat listener function that makes some pads blink on the beat.
Code: Select all
import Live
from _Framework.ControlSurface import ControlSurface
from _Framework.ButtonMatrixElement import ButtonMatrixElement
from _Framework.ButtonElement import ButtonElement
from _Framework.InputControlElement import MIDI_NOTE_TYPE
from _Framework.Resource import PrioritizedResource
from _APC.SkinDefault import make_biled_skin, make_default_skin, make_stop_button_skin
from _APC.SkinDefault import GREEN, GREEN_BLINK, RED, RED_BLINK, AMBER
MANUFACTURER_ID = 71
ABLETON_MODE = 65
DO_COMBINE = Live.Application.combine_apcs()
SESSION_WIDTH = 8
SESSION_HEIGHT = 5
MIXER_SIZE = 8
logger = None
def log(msg):
global logger
if logger is not None:
logger(msg)
class APC40_KEY(ControlSurface):
"""Controller minimale per APC40 che gestisce l'handshake con il controller."""
def __init__(self, c_instance):
global logger
logger = self.log_message
super(APC40_KEY, self).__init__(c_instance)
log("APC40_KEY: Initializing")
self._suppress_send_midi = True
self._suppress_session_highlight = True
self._suggested_input_port = 'Akai APC40'
self._suggested_output_port = 'Akai APC40'
self._dongle_challenge = (
Live.Application.get_random_int(0, 2000000),
Live.Application.get_random_int(2000001, 4000000)
)
self._device_id = 0
# self.refresh_state()
self._color_default_skin = make_default_skin()
self._color_skin = make_biled_skin()
self._stop_button_color_skin = make_stop_button_skin()
self._beat_tick = 0
self._pads = []
self._scene_launch_pads = []
self._stop_pads = []
with self.component_guard():
self._create_pad_matrix()
self._create_scene_launch_pads()
self._create_stop_pads()
self.song().add_current_song_time_listener(self.beat_listener)
# Not sure why, but colors initialization works only with a delay > 5 (maybe because the first refresh_state() has a delay of 5??)
self.schedule_message(10, self._init_pads_colors)
def _product_model_id_byte(self):
# Original APC40 model ID
return 115
def handle_sysex(self, midi_bytes):
self._suppress_send_midi = False
if midi_bytes[3] == 6 and midi_bytes[4] == 2:
self._on_identity_response(midi_bytes)
elif midi_bytes[4] == 81:
self._on_dongle_response(midi_bytes)
def _on_identity_response(self, midi_bytes):
if midi_bytes[5] == MANUFACTURER_ID and midi_bytes[6] == self._product_model_id_byte():
# Estrae i byte della versione (i byte 9-12) e l'ID del dispositivo (byte 13)
version_bytes = midi_bytes[9:13]
self._device_id = midi_bytes[13]
self._send_introduction_message()
challenge1 = [ (self._dongle_challenge[0] >> (4 * (7 - index))) & 15 for index in range(8)]
challenge2 = [ (self._dongle_challenge[1] >> (4 * (7 - index))) & 15 for index in range(8)]
dongle_message = (240,
MANUFACTURER_ID,
self._device_id,
self._product_model_id_byte(),
80,
0,
16) + tuple(challenge1) + tuple(challenge2) + (247,)
self._send_midi(dongle_message)
msg = ("APC40_KEY: Got identity response from controller, version {}.{}"
.format((version_bytes[0] << 4) + version_bytes[1],
(version_bytes[2] << 4) + version_bytes[3]))
self.log_message(msg)
self._init_pads_colors()
def _on_dongle_response(self, midi_bytes):
if (midi_bytes[1] == MANUFACTURER_ID and
midi_bytes[3] == self._product_model_id_byte() and
midi_bytes[2] == self._device_id and
midi_bytes[5] == 0):
if midi_bytes[6] == 16:
response = [0, 0]
for index in range(8):
response[0] += (midi_bytes[7 + index] & 15) << (4 * (7 - index))
response[1] += (midi_bytes[15 + index] & 15) << (4 * (7 - index))
expected = Live.Application.encrypt_challenge(self._dongle_challenge[0],
self._dongle_challenge[1])
# Compare received response with expected
if [int(expected[0]), int(expected[1])] == response:
self._on_handshake_successful()
def _on_handshake_successful(self):
self._suppress_session_highlight = False
self.log_message("APC40_KEY: Handshake successful; control surface enabled.")
def _send_introduction_message(self, mode_byte=ABLETON_MODE):
version = (self.application().get_major_version(),
self.application().get_minor_version(),
self.application().get_bugfix_version())
intro_msg = (240,
MANUFACTURER_ID,
self._device_id,
self._product_model_id_byte(),
96,
0,
4,
mode_byte,
version[0],
version[1],
version[2],
247)
self._send_midi(intro_msg)
def _send_midi(self, midi_bytes, optimized=None):
if not self._suppress_send_midi:
return ControlSurface._send_midi(self, midi_bytes, optimized=optimized)
return False
def refresh_state(self):
self.schedule_message(5, self._update_hardware)
def _update_hardware(self):
self._suppress_send_midi = True
with self.component_guard():
for component in self.components:
component.set_enabled(False)
self._suppress_send_midi = False
self._send_midi((240, 126, 0, 6, 1, 247))
# ---------------------------------------------------------------------------------
def _create_pad_matrix(self):
# self._pad_matrix = ButtonMatrixElement(name="Pad_Matrix")
for scene_index in range(SESSION_HEIGHT):
row = []
for track_index in range(SESSION_WIDTH):
button = ButtonElement(
is_momentary=True,
msg_type=MIDI_NOTE_TYPE,
channel=track_index, # midi channel
identifier=scene_index + 53, # midi note
name="%d_Clip_%d_Button" % (scene_index, track_index),
skin=self._color_skin)
# First half of pads with color red, the other half with color orange
idle_color = 'Session.ClipRecording' if track_index < 4 else 'Session.ClipStopped'
button.add_value_listener(lambda value, btn=button, idle_color=idle_color: self.on_pad_value(value, btn, idle_color))
row.append(button)
self._pads.append(row)
log("Pad matrix 8x5 initialized")
def on_pad_value(self, value, button, idle_color):
if value:
button.set_light('Session.ClipStarted') # green on pressed
else:
button.set_light(idle_color)
def _init_pads_colors(self):
if self._pads:
for scene_index in range(SESSION_HEIGHT):
for track_index in range(SESSION_WIDTH):
button = self._pads[scene_index][track_index]
# First half of pads with color red, the other half with color orange
if track_index < 4:
button.set_light('Session.ClipRecording')
else:
button.set_light('Session.ClipStopped')
def _create_scene_launch_pads(self):
for scene_index in range(SESSION_HEIGHT):
button = ButtonElement(
is_momentary=False,
msg_type=MIDI_NOTE_TYPE,
channel=0, # midi channel
identifier=scene_index + 82, # midi note
name=f'Scene_{scene_index}_Launch_Button',
skin=self._color_skin)
# Apparently if we don't add a listener, we can't change the color of the button...Another mystery
button.add_value_listener(lambda value, btn=button: self._empty_listener(btn))
self._scene_launch_pads.append(button)
log("Scene launch pads initialized")
def _create_stop_pads(self):
for track_index in range(SESSION_WIDTH):
button = ButtonElement(
is_momentary=False,
msg_type=MIDI_NOTE_TYPE,
channel=track_index, # midi channel
identifier=52, # midi note
name=f'Scene_{track_index}_Stop_Button',
skin=self._color_skin)
# Apparently if we don't add a listener, we can't change the color of the button...Another mystery
button.add_value_listener(lambda value, btn=button: self._empty_listener(btn))
self._stop_pads.append(button)
log("Stop pads initialized")
def _empty_listener(self, o):
pass
def beat_listener(self):
prev_beat_tick = self._beat_tick
# the sub_division ticks counts from 1 to 4 over a single beat
# https://docs.cycling74.com/legacy/max8/vignettes/live_object_model#live_obj_anchor_Song
self._beat_tick = self.song().get_current_beats_song_time().sub_division
if prev_beat_tick != self._beat_tick:
# beat on
if self._beat_tick == 1:
for pad in self._scene_launch_pads:
pad.set_light('Session.ClipStarted')
for pad in self._stop_pads:
pad.set_light('Session.ClipStarted')
# beat off
elif self._beat_tick == 2:
for pad in self._scene_launch_pads:
pad.set_light('Session.ClipEmpty')
for pad in self._stop_pads:
pad.set_light('Session.ClipEmpty')
One issue I am facing now is that when I assign one of the pad using Ableton midi map mode, it loses the color.
Even if make the function _init_pads_colors() running again after the midi map, it just ignores the color received for that pad. Does anyone know why?