Replace Live's MIDI mappings with MIDI remote scripts

Discuss music production with Ableton Live.
Post Reply
jbone1313
Posts: 587
Joined: Wed Dec 12, 2007 2:44 am
Contact:

Replace Live's MIDI mappings with MIDI remote scripts

Post by jbone1313 » Tue Mar 02, 2021 4:43 pm

As you probably know, Live's MIDI mappings are tightly coupled to specific Live sets. Decoupling MIDI mappings from specific Live sets is a tantalizing prospect. The purpose of this thread is to discuss whether that is possible and how it can be accomplished.

Some of the advantages of decoupling MIDI mappings from Live sets are:
  • Mental bandwidth: not having to worry about which MIDI mappings are in which sets is mentally freeing. Yes, templates can help, but they are still not ideal
  • Data preservation: when sets get corrupted or crash, MIDI mappings can be lost. Yes, backups can help, but they are still not ideal
In this thread, I was able to get MIDI buttons mapped to specific Live dials, which is great. My next attempt is to simply map an encoder to a specific device dial on a specific track. Given how easy it was to map buttons, I thought encoders would be easy, but they are not. From what I can tell, _Framework DeviceComponent is geared towards blue hand mapping, not specific MIDI mapping.

In theory, this is possible. The Remotify people have done something like this.

Getting buttons and encoders mapped to specific Live dials would be a long way down the road of achieving this goal.

Anybody up for this?

Below are the best links I have found for getting into MIDI remote scripting.

viewtopic.php?f=1&t=242270
http://remotescripts.blogspot.com/2010/ ... asses.html
https://structure-void.com/AbletonLiveR ... Framework/
https://structure-void.com/PythonLiveAP ... ve11.0.xml
https://github.com/gluon/AbletonLive11_ ... oteScripts
https://docs.cycling74.com/max8/vignett ... ject_model
Sonoran Music Devices
Monsoon (a chord track for Ableton Live) and other devices are available here:
https://sonoranmusicdevices.gumroad.com/

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

Re: Replace Live's MIDI mappings with MIDI remote scripts

Post by S4racen » Tue Mar 02, 2021 5:04 pm

Yes it's already possible and available as ClyphX Pro's bindings feature.

Cheers
D

jbone1313
Posts: 587
Joined: Wed Dec 12, 2007 2:44 am
Contact:

Re: Replace Live's MIDI mappings with MIDI remote scripts

Post by jbone1313 » Tue Mar 02, 2021 5:09 pm

ClyphX Pro is cool but:
a) It currently does not work in Live 11
b) Not having to rely on a third-party script for this would be great. See a) above
Sonoran Music Devices
Monsoon (a chord track for Ableton Live) and other devices are available here:
https://sonoranmusicdevices.gumroad.com/

lu57
Posts: 26
Joined: Mon Jan 20, 2020 9:02 am
Contact:

Re: Replace Live's MIDI mappings with MIDI remote scripts

Post by lu57 » Tue Mar 02, 2021 6:25 pm

jbone1313 wrote:
Tue Mar 02, 2021 5:09 pm
ClyphX Pro is cool but:
a) It currently does not work in Live 11
b) Not having to rely on a third-party script for this would be great. See a) above
Hi! I maintain ClyphX free, and Live 11 support is nearly done :)
https://github.com/ldrolez/clyphx-live10

jbone1313
Posts: 587
Joined: Wed Dec 12, 2007 2:44 am
Contact:

Re: Replace Live's MIDI mappings with MIDI remote scripts

Post by jbone1313 » Tue Mar 02, 2021 7:10 pm

Here it is. A POC of what I was looking for.

I may need to do some normalizing/converting of MIDI values to parameter values, but that won't be hard.

When I get everything working and cleaned up, I will post.

Usual disclaimer: This code is distributed WITHOUT ANY WARRANTY WHATSOEVER. Use at your own risk.

Code: Select all

from _Framework.ControlSurface import ControlSurface
from _Framework.ButtonElement import ButtonElement
from _Framework.EncoderElement import EncoderElement
from _Framework.InputControlElement import *
import Live

class MyControlSurface(ControlSurface):
    
    def __init__(self, instance):
            
        super(MyControlSurface, self).__init__(instance, False)
         
        with self.component_guard():
            #self.log_message("MyControlSurface loaded")
            self.show_message("MyControlSurface loaded")

            encoder = EncoderElement(MIDI_CC_TYPE, 0, 1, Live.MidiMap.MapMode.relative_smooth_two_compliment)   
            encoder.add_value_listener(self.encoderTurned)    

    def encoderTurned(self, value):
        for track in self.song().tracks:
                for device in track.devices:
                        if device.name.upper() == "MyTEST":
                                for parameter in device.parameters:                
                                        if parameter.name.upper() == "TEST1":
                                                parameter.value = value
   
Sonoran Music Devices
Monsoon (a chord track for Ableton Live) and other devices are available here:
https://sonoranmusicdevices.gumroad.com/

jbone1313
Posts: 587
Joined: Wed Dec 12, 2007 2:44 am
Contact:

Re: Replace Live's MIDI mappings with MIDI remote scripts

Post by jbone1313 » Fri Mar 05, 2021 8:31 pm

This is a follow up. Now that I have it working, I am coming back with some how tos.

Huge disclaimer: Any of the below code or other text is provided without any warranties whatsoever. Use at your own risk.

Being able to control Live like this is incredible.

I have many years of programming experience, but I have no Python experience until now. I found it relatively easy to pick up.

I would encourage anyone who is both interested in Live and who is not yet a programmer, but who is interested in learning programming to try this out. It would be a great way to learn programming with something that you can relate to.

Basically, what my code does is MIDI-map buttons and encoders to Live devices, just like regular MIDI mapping.

Unlike Live's MIDI mapping, a huge advantage to my code is that it can map to any device with the same matching name. That is particularly useful for my scenario, because I have a lot of the same MIDI effects on tracks. Being able to control them all at the same time without tedious MIDI mappings is simply awesome. Also, I have tracks with dedicated Drum Racks, which send MIDI data to External Instrument devices. So, essentially, any Live track can be its own MIDI controller. Combine that with Push, and wow. It is hard to overstate how powerful that is.

My code also has listeners so that when devices are added and parameters are renamed, it updates itself automatically. So, if you are using the multi map thing I described above and you add a new device, it will include that.

I took my main code and attempted to reduce it down to some simple examples, and I posted it below. You will notice that the parameters are stored in lists. That is because of the multiple parameter mapping stuff I described above.

The below code requires the init script. See the first link above for how to set that up.

The loadParameter function is a big loop, which handles multiple levels of devices. You may wish to customize this for your needs.

Code: Select all

from ableton.v2.control_surface import IdentifiableControlSurface, MIDI_CC_TYPE
from ableton.v2.control_surface.elements import ButtonElement, EncoderElement
import Live
import logging
logger = logging.getLogger(__name__)

class MyControlSurface(IdentifiableControlSurface):
    def __init__(self, *a, **k):
        
        super(MyControlSurface, self).__init__( *a, **k)
  
        with self.component_guard():

            self.parameterListMain = [] 

            self.parameterCh1CC1s = []
            self.parameterCh1CC2s = []
            self.parameterCh1CC6s = []
            self.parameterCh1CC7s = []
            
            buttonPitchDropOff = ButtonElement(False, 0, 0, 60).add_value_listener(self.buttonPressedPitchDropOff)
            buttonPitchDropOn = ButtonElement(False, 0, 0, 61).add_value_listener(self.buttonPressedPitchDropOn)

            encoderBusFX1Macro1 = EncoderElement(MIDI_CC_TYPE, 0, 6, Live.MidiMap.MapMode.absolute).add_value_listener(self.encoderTurnedBusFX1Macro1)       
            encoderBusFX1Macro2 = EncoderElement(MIDI_CC_TYPE, 0, 7, Live.MidiMap.MapMode.absolute).add_value_listener(self.encoderTurnedBusFX1Macro2)       
                           
            self.song.add_tracks_listener(self.trackChanged)
            
            for track in self.song.tracks:
                track.add_devices_listener(self.deviceChanged)
                for deviceLevel0 in track.devices:
                    deviceLevel0.add_name_listener(self.deviceLevel0NameChanged)
         
            self.loadParameters()

            logger.info("MyControlSurface loaded")
            self.show_message("MyControlSurface loaded")

    def buttonPressedPitchDropOff(self, value): 
        self.setParameter(self.parameterCh1CC1s, 0)
        self.setParameter(self.parameterCh1CC2s, 0)

    def buttonPressedPitchDropOn(self, value): 
        self.setParameter(self.parameterCh1CC1s, 1)
        self.setParameter(self.parameterCh1CC2s, 127)

    def encoderTurnedBusFX1Macro1(self, value):self.setParameter(self.parameterCh1CC6s, value)         
    def encoderTurnedBusFX1Macro2(self, value):self.setParameter(self.parameterCh1CC7s, value)         
    
    def loadParameter(self, deviceNameLevel0, chainNameLevel0, deviceNameLevel1, chainNameLevel1, deviceNameLevel2, parameterName, parameterList):       
        parameterList.clear()
        
        for track in self.song.tracks:
                for deviceLevel0 in track.devices:
                        if deviceLevel0.name.upper() == deviceNameLevel0.upper():
                                if chainNameLevel0 == None:
                                        for parameter in deviceLevel0.parameters:                
                                            self.appendParameter(parameterList, parameterName, parameter)
                                else:
                                        for chainLevel0 in deviceLevel0.chains:
                                                if chainLevel0.name.upper() == chainNameLevel0.upper():
                                                        for deviceLevel1 in chainLevel0.devices:
                                                                if deviceLevel1.name.upper() == deviceNameLevel1.upper():
                                                                        if chainNameLevel1 == None:
                                                                                for parameter in deviceLevel1.parameters:                
                                                                                    self.appendParameter(parameterList, parameterName, parameter)
                                                                        else:
                                                                                for chainLevel1 in deviceLevel1.chains:
                                                                                        if chainLevel1.name.upper() == chainNameLevel1.upper():
                                                                                                for deviceLevel2 in chainLevel1.devices:
                                                                                                        if deviceLevel2.name.upper() == deviceNameLevel2.upper():
                                                                                                                for parameter in deviceLevel2.parameters:                
                                                                                                                    self.appendParameter(parameterList, parameterName, parameter)

    def appendParameter(self, parameterList, parameterName, parameter):
        if (parameterName.upper() == "DEVICE ON" and parameter.name.upper().startswith('DEVICE ON')) or parameter.name.upper() == parameterName.upper():
            if not parameter.name_has_listener(self.parameterNameChanged):
                parameter.add_name_listener(self.parameterNameChanged)
            parameterList.append(parameter)
            self.parameterListMain.append(parameter)
            
    def setParameter(self, parameterList, value):
        for parameter in parameterList:
            parameter.value = value
    
    def loadParameters(self):
        self.parameterListMain.clear()
        
        self.loadParameter("Control Bus FX 1", None, None, None, None, "Bus FX 1 Macro 1", self.parameterBusFXControlControlBusFX1Macro1s)
        self.loadParameter("Control Bus FX 1", None, None, None, None, "Bus FX 1 Macro 2", self.parameterBusFXControlControlBusFX1Macro2s)

        self.loadParameter("Pitch Drop", None, None, None, None, "Device On", self.parameterCh1CC1s)
        self.loadParameter("Pitch Drop", None, None, None, None, "Drop Activate", self.parameterCh1CC2s)

    def trackChanged(self):
        for track in self.song.tracks:
            if not track.devices_has_listener(self.deviceChanged):
                track.add_devices_listener(self.deviceChanged)

    def deviceChanged(self):
        for track in self.song.tracks:
            for deviceLevel0 in track.devices:
                if not deviceLevel0.name_has_listener(self.deviceLevel0NameChanged):
                    deviceLevel0.add_name_listener(self.deviceLevel0NameChanged)

        self.loadParameters()
    
    def deviceLevel0NameChanged(self):
        self.loadParameters()
    
    def parameterNameChanged(self):
        self.loadParameters()
    
    def getDeviceLevel0(self, trackName, deviceLevel0Name):
        for track in self.song.tracks:
            if track.name.upper() == trackName.upper():
                for deviceLevel0 in track.devices:
                    if deviceLevel0.name.upper() == deviceLevel0Name.upper():
                        return deviceLevel0        

    def removeListeners(self):
        if self.song.tracks_has_listener(self.trackChanged):
            self.song.remove_tracks_listener(self.trackChanged)
            
        for track in self.song.tracks:
            if track.devices_has_listener(self.deviceChanged):
                track.remove_devices_listener(self.deviceChanged)
            for deviceLevel0 in track.devices:
                if deviceLevel0.name_has_listener(self.deviceLevel0NameChanged):
                    deviceLevel0.remove_name_listener(self.deviceLevel0NameChanged)

        for parameter in self.parameterListMain:       
            if parameter.name_has_listener(self.parameterNameChanged):
                    parameter.remove_name_listener(self.parameterNameChanged)

    def disconnect(self):
        self.removeListeners()
        
        self.parameterListMain = None 

        self.parameterCh1CC1s = None
        self.parameterCh1CC2s = None
        self.parameterCh1CC6s = None
        self.parameterCh1CC7s = None
            
        super(MyControlSurface, self).disconnect()
        logger.info("MyControlSurface unloaded")
Last edited by jbone1313 on Mon Mar 08, 2021 7:26 pm, edited 3 times in total.
Sonoran Music Devices
Monsoon (a chord track for Ableton Live) and other devices are available here:
https://sonoranmusicdevices.gumroad.com/

jbone1313
Posts: 587
Joined: Wed Dec 12, 2007 2:44 am
Contact:

Re: Replace Live's MIDI mappings with MIDI remote scripts

Post by jbone1313 » Mon Mar 08, 2021 4:17 pm

I successfully switched from _Framework to ableton.v2. I updated the above script accordingly. Also, the init script looks like this now:

Code: Select all

from .MyControlSurface import MyControlSurface

def create_instance(c_instance):
    return MyControlSurface(c_instance=c_instance)
Sonoran Music Devices
Monsoon (a chord track for Ableton Live) and other devices are available here:
https://sonoranmusicdevices.gumroad.com/

randi
Posts: 116
Joined: Thu Apr 03, 2014 4:03 am
Contact:

Re: Replace Live's MIDI mappings with MIDI remote scripts

Post by randi » Mon Mar 08, 2021 7:05 pm

Thanks for your postings here, very helpful and appreciated!
Couple of questions:
1. Do you know if v2 and _framework can be used together / at the same time ?
2. Why did switch? Any advantages or more stuff in v2?
3. How are the two different?
Thanks in advance 😊

jbone1313
Posts: 587
Joined: Wed Dec 12, 2007 2:44 am
Contact:

Re: Replace Live's MIDI mappings with MIDI remote scripts

Post by jbone1313 » Mon Mar 08, 2021 7:11 pm

randi wrote:
Mon Mar 08, 2021 7:05 pm
Thanks for your postings here, very helpful and appreciated!
Couple of questions:
1. Do you know if v2 and _framework can be used together / at the same time ?
2. Why did switch? Any advantages or more stuff in v2?
3. How are the two different?
Thanks in advance 😊
1. Not sure. From what I can tell, it *seems* like v2 is a newer replacement for _Framework, so using them in the same script might not make sense. It seems like older control surfaces still use _Framework, e.g., APC40.
2. See above. I *reckon* v2 will live on longer and be more forward compatible. So, I felt like trying to see if I could get it to work. And it did.
3. See above. From what I can tell, they are not much different, at least for my needs. I *imagine* v2 might have some newer capabilities for more advanced stuff. I dunno.

:)
Sonoran Music Devices
Monsoon (a chord track for Ableton Live) and other devices are available here:
https://sonoranmusicdevices.gumroad.com/

randi
Posts: 116
Joined: Thu Apr 03, 2014 4:03 am
Contact:

Re: Replace Live's MIDI mappings with MIDI remote scripts

Post by randi » Mon Mar 08, 2021 7:32 pm

Thanks for the reply!
I'm in the middle of something here with the Mackie C4 (https://github.com/markusschloesser/MackieC4_P3/) and will need to tidy up at some point :wink:

lee__yosh
Posts: 3
Joined: Wed Oct 04, 2023 8:13 am

Re: Replace Live's MIDI mappings with MIDI remote scripts

Post by lee__yosh » Wed Oct 04, 2023 9:37 am

Most of what I found on the net for Ableton 11, the script for Block11 https://github.com/leonardreidy/Block11 ... in/Block11 suited me in terms of functionality, but there is one problem - all the Encoders on my faderfox FT3, except for the one assigned to regulate the tempo, do not want to work correctly with this script. It looks like the question is in "map mode" which is written there only to regulate the tempo in the file SpecialTransportComponent.py (line 134), and for the rest "sends" "volums" "device parameters" ... MapMode seems to be defined as "Absolute" and I don't know how to change it to "TwoCompliment"

Image
Please help me edit the script. I need to change "map mode" to "Live.MidiMap.MapMode.relative_two_compliment"

lee__yosh
Posts: 3
Joined: Wed Oct 04, 2023 8:13 am

Re: Replace Live's MIDI mappings with MIDI remote scripts

Post by lee__yosh » Thu Oct 05, 2023 7:41 pm

thanks to all forum participants for their help :-) today I found a solution by adding + User remote midi script to the concept taken from the "Block11" script!
P.S.
Now I have 2 device controllers on one MIDI controller (on each of the scripts) one of which can be used to control 16 parameters of a dedicated rack, for example, and the second can be attached to the master for quick access to managing plugins like Ozone. Light feedback also somehow magically worked, everything worked correctly, it even displays which bank with parameters for managing plugins you are in :-) and in general, if anyone is interested in a working midi script for Faderfox FT3, write here, I’ll share!

Post Reply