APC40: initialize pads colors at start

Discuss Live-ready controllers other than Push.
Post Reply
spinlud
Posts: 105
Joined: Sat May 26, 2012 9:27 am

APC40: initialize pads colors at start

Post by spinlud » Fri Mar 14, 2025 1:03 pm

Hi, as a test I am simply trying to initialize APC40 8x5 matrix pads to a color (orange) and attach a listener to change color to red when pressed and back to orange when released. The listener part works, but the color initialization doesn't (pads stay black). Not sure why since the code to change color is the same:

Code: Select all

    def _create_pad_matrix(self):                
        self._pad_matrix = ButtonMatrixElement(name="Pad_Matrix")
        self._pads = []
        
        for scene_index in range(SESSION_HEIGHT):
            pad_row = []
            for track_index in range(SESSION_WIDTH):                
                pad = 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)
                                          
                pad.add_value_listener(lambda value, btn=pad: self.on_pad_value(value, btn))

                self._pads.append(pad)

        # Tying to initialize pads colors with some delay...no luck
        self.schedule_message(5, self._init_pads_colors)             
        log("Pad matrix 8x5 initialized")

    def _init_pads_colors(self):
        # This does not work (pads stay black)
        for pad in self._pads:
            pad.set_light('Session.ClipStopped') # orange
        log("Pads colors initialized")

    # This works (red when pressed / orange when released)
    def on_pad_value(self, value, button):        
        log(f'{value} {button}')
        if value:
            button.set_light('Session.ClipRecording') # red when pressed
        else:
            button.set_light('Session.ClipStopped') # orange when released
Any idea why I can't change color at start and need an interaction to change the color?

spinlud
Posts: 105
Joined: Sat May 26, 2012 9:27 am

Re: APC40: initialize pads colors at start

Post by spinlud » Wed Mar 19, 2025 4:47 pm

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? 🤔

S4racen
Posts: 5947
Joined: Fri Aug 24, 2007 4:08 pm
Location: Dunstable
Contact:

Re: APC40: initialize pads colors at start

Post by S4racen » Wed Mar 19, 2025 6:16 pm

Can't MIDI Map and use a control surface....

Cheers
D

spinlud
Posts: 105
Joined: Sat May 26, 2012 9:27 am

Re: APC40: initialize pads colors at start

Post by spinlud » Wed Mar 19, 2025 7:00 pm

S4racen wrote:
Wed Mar 19, 2025 6:16 pm
Can't MIDI Map and use a control surface....

Cheers
D
Hi, thanks for the feedback!
The thing is that I can see in the midi monitor the midi messages sent to the controller to change the color but they have no effect (only for the mapped parameters). So it seems the remote script is still sending the midi messages. Not sure how Ableton can make the controller to ignore the midi message received :?:

S4racen
Posts: 5947
Joined: Fri Aug 24, 2007 4:08 pm
Location: Dunstable
Contact:

Re: APC40: initialize pads colors at start

Post by S4racen » Thu Mar 20, 2025 8:39 am

MIDI Mapping disconnects any control surface messages, simple as that. Same with every controller that allows it...
Cheers
D

spinlud
Posts: 105
Joined: Sat May 26, 2012 9:27 am

Re: APC40: initialize pads colors at start

Post by spinlud » Sat Mar 29, 2025 5:39 pm

S4racen wrote:
Thu Mar 20, 2025 8:39 am
MIDI Mapping disconnects any control surface messages, simple as that. Same with every controller that allows it...
Cheers
D
Yeah you are right, I don't know why I had the impression that the api listener was still working after the midi assignment, but actually I was wrong it stops been invoked after the assignment

Post Reply