Scripting long press support for MIDI controller - Possible?

Setup: I have a MIDI footswitch (Hotone Ampero Control) controlling various widgets in my gig preset. It works flawlessly for one function per switch, absolutely no issues making it do what I want via the widget settings in GP.

However, I would love to be able to program a long press function for each switch as well, and I’m wondering if that’s going to be realistically possible.

The MIDI controller allows me to program different messages for press and release for each switch (so for example I can send CC#1 127 on press and CC#2 0) on release. I suspect this will be vital if this is going to work.

My initial thought was that maybe I could do something like this:

  • I set up each switch to send one message for press and a different for relase, for example CC1 for press and CC2 for release, both with values of 127
  • My widgets are set to react to the release message
  • When my script detects the press message, it starts a timer.
  • If a release message isn’t detected within a certain time limit (e.g. 2 seconds), the script determines we are in a long press situation, and triggers whatever the long press event should be

The problem:
This proposed setup will require the script to be able to cancel the release message somehow, so that when the release message is sent and the script has already triggered a long press event, the short press event should not trigger. Is this possible? Or am I looking at this all wrong?

I did find this thread, which I’m currently trying to parse and fully understand to see if it’s useful for me:

However, on first glance it looks like this might interfere with the Widget functionality of GP, because it processed everything in code and does not use widgets that are bound to plugin parameters at all. This means that I might lose all my visual indicators about what state my preset is in (what effects are turned on and off, etc), which will not work for me. I absolutely need to keep my widget visual feedback intact (for short press functions - don’t need it for long press, since that will trigger stuff like tuner, variation change, etc)

This kind of thing is certainly doable in a script (or through the external API). However, I would think you’re best off doing it by not attaching widgets directly to the midi events from your controller.

You would have to have only the script set up to receive the midi callbacks. It would start the timer on receipt of a note on, for example, and then react again either after a certain period of time, or when the note-off arrives.

As long as they are distinct midi events there’s really no need to use a different note number or cc for the on and off. Typical button pushes are something like cc 25 127 for press and cc 25 0 for release. I think you’d be making things more complicated by changing the cc number for press and release.

Once your script has figured out the proper action to take it would change the widget directly.

1 Like

First, this has nothing to do with widgets. You intercept the midi from the footswitch in the gig script, do whatever you want with it, then either suppress or substitute or just pass through by injecting into a midi input. So your downstream scripts will not suspect anything. Those processed messages can be mapped to widget midi just like normal ones can.

So you detect key down, start the timer, if it’s under 2 seconds, send one message, if it’s over 2 seconds, send another one. It works, in general, and of course you can do it for as many hardware buttons as you like.

However, it’s not all super trivial for a number of reasons.

First, you’ll sometimes need to send not one but two messages, emulating key down and subsequent key up, otherwise your widgets can stay “stuck” in the on position. And gig scripts for whatever reason don’t have SendLater, so the logic becomes quite cumbersome and more complex than anyone needs.

Second, I tried using long press the way you describe it, but then decided I don’t like it because then any action will happen with a delay (you can of course tap those buttons very quickly but still).

Third, I found that particular script with three banks confusing because it’s hard to know where you are.

So what I ended up doing is having two banks, one switches song parts, the other one controls the looper. Everything works on key down, so if I’m in the lower bank and press an already selected song part it’s ignored once, multiple presses work as tap tempo, long press goes bank up, to looper control. From there long press gets me back to bank one. Also, if I have a loop recorded but stopped, pressing song part buttons in the lower bank starts and stops looper once. I have to press buttons that don’t interrupt anything, but it’s quite easy because they are typically lit up or something. But the action is instant, and the logic is easier.

1 Like

Here’s the scripting part where I handle the footswitch (actual song part changes and looper control happen in a global rackspace script downstream, if I remember correctly). The script here is used in the gig script via an Include statement. I need to clean it up, I know :smiley:

Var 
    LPSidle: Integer = 0 //status before long press is registered
    LPSover: Integer = 1 //status after long press is registered
    LongTime: Integer = 1000 //time to wait to register long press

    LongPress: Ramp //timer for the footswitch
    LongPressState: Integer = LPSidle //initialization of status
    CCBuffer: Integer
    ChannelBuffer: Integer

    looperCounter: Integer = 0 //counter for looper
    
    //IsBankUp: Boolean = false //initialization of bank up or down flag

    LEDOn: Ramp //timer for LED blinking

Function BlinkFS (time: Integer) //this function blinks LEDs
    If MidiOutDeviceExists (FSOUT) == True Then
        SendNowToMidiOutDevice (FSOUT, MakeControlChangeMessageEx(CCA, 127, FSChan)) //turning LEDs on
        SendNowToMidiOutDevice (FSOUT, MakeControlChangeMessageEx(CCB, 127, FSChan))
        SendNowToMidiOutDevice (FSOUT, MakeControlChangeMessageEx(CCC, 127, FSChan))
        SendNowToMidiOutDevice (FSOUT, MakeControlChangeMessageEx(CCD, 127, FSChan))
        TriggerOneShotRamp(LEDOn, time, 10) //starting timer
    End
End

Function FSTap (cc: Integer, ccv: Integer)
If InSetlistMode () == True And looperCounter > 0 And IsBankUp == False And ccv == 127 And cc > 19 And cc < 24 Then
    OSC_SendCommandSpecific ("/looperTrigger", oscip, oscport)
    looperCounter = looperCounter - 1
End
If InSetlistMode () == True Then
        Select
            cc == 20 And GetCurrentSongPartIndex () == 0 Do
                If ccv == 127 Then 
                    Tap ()
                    //OSC_SendCommandSpecific ("/tapon", oscip, oscport)
                Elsif spchanged == True Then
                    spchanged = False
                Else
                    //OSC_SendCommandSpecific ("/tapoff", oscip, oscport)
                End
            cc == 21 And GetCurrentSongPartIndex () == 1 Do
                If ccv == 127 Then 
                    Tap ()
                    //OSC_SendCommandSpecific ("/tapon", oscip, oscport)
                Elsif spchanged == True Then
                    spchanged = False
                Else
                    //OSC_SendCommandSpecific ("/tapoff", oscip, oscport)
                End
            cc == 22 And GetCurrentSongPartIndex () == 2 Do
                If ccv == 127 Then 
                    Tap ()
                    //OSC_SendCommandSpecific ("/tapon", oscip, oscport)
                Elsif spchanged == True Then
                    spchanged = False
                Else
                    //OSC_SendCommandSpecific ("/tapoff", oscip, oscport)
                End
            cc == 23 And GetCurrentSongPartIndex () == 3 Do
                If ccv == 127 Then 
                    Tap ()
                    //OSC_SendCommandSpecific ("/tapon", oscip, oscport)
                Elsif spchanged == True Then
                    spchanged = False
                Else
                    //OSC_SendCommandSpecific ("/tapoff", oscip, oscport)
                End
            //True Do
            //    InjectMidiEvent (FSOUT, c)
        End
    End
End

On GeneratorEndCycle (timeX: Integer) from LEDOn //turning LEDs off, sending control message to racks, toggling bank flag 

       If IsBankUp == false Then 
        InjectMidiEvent (FSOUT, MakeControlChangeMessageEx(ControlCC, 127, BankUpChannel)) //sending control message
            Notify("Footswitch Bank 2")
       Else
            InjectMidiEvent (FSOUT, MakeControlChangeMessageEx(ControlCC, 0, BankUpChannel))
            Notify("Footswitch Bank 1")
       End
       IsBankUp = !IsBankUp //changing bank flag

End

On ControlChangeEvent (m: ControlChangeMessage) from SWITCH //intercepting key presses

    Var
        OldCC: Integer = GetCCNumber(m)
        OldValue: Integer = GetCCValue(m)
        OldChannel: Integer = GetChannel(m)
        //Print("OldCC " + OldCC + " OldValue " + OldValue + " OldChannel " + OldChannel + " IsBankUp " + IsBankUp )
   
    If OldValue > 64 Then //detecting button down
        
        If IsBankUp == false Then CCBuffer = OldCC
        Else CCBuffer = OldCC + 95 End
        If IsBankUp == false Then ChannelBuffer = OldChannel
        Else ChannelBuffer = BankUpChannel End
        //Print ("key down, CCBuffer " + CCBuffer + " ChannelBuffer " + ChannelBuffer)
        LongPressState = LPSidle
        TriggerOneShotRamp(LongPress, LongTime, 10) //starting timer when the button goes down
        
    Else
        StopOneShotRamp(LongPress)
    End
    //Print("lps is " + LongPressState)
    //Print("key up happened is " + KeyUpHappened)
    If IsBankUp == true And LongPressState == LPSidle /*And InLongPress == false*/ Then //checking if the upper bank is on to rewrite messages
        //If LongPressState == LPSidle Then Print("lps idle, injecting rewritten message") Else Print ("lps not idle, suppressing rewritten message") End
        Select
            OldCC == CCA Do
                InjectMidiEventViaRigManager (SWITCH, MakeControlChangeMessageEx(BankUpCCA, OldValue, BankUpChannel)) //reassigning to new channel and CC#
            OldCC == CCB Do
                InjectMidiEventViaRigManager (SWITCH, MakeControlChangeMessageEx(BankUpCCB, OldValue, BankUpChannel))
            OldCC == CCC Do
                InjectMidiEventViaRigManager (SWITCH, MakeControlChangeMessageEx(BankUpCCC, OldValue, BankUpChannel))
            OldCC == CCD Do
                InjectMidiEventViaRigManager (SWITCH, MakeControlChangeMessageEx(BankUpCCD, OldValue, BankUpChannel))
        End
    Else
        If LongPressState == LPSidle /*And InLongPress == false And KeyUpHappened == true*/ Then
            //Print("lps idle and in long press is, injecting original message")
            InjectMidiEventViaRigManager (SWITCH, m) //if no reassignment is needed, sending the message as is
            FSTap (OldCC, OldValue)
        End
    End

End


On GeneratorEndCycle (timeX: Integer) from LongPress //checking the timer, changing state, blinking LEDs to indicate bank change 
    //Print("long cycle end event triggered")
    If LongPressState == LPSidle Then
        LongPressState = LPSover //changing state if button is pressed for long enough 
        //Print("changing state to over, long press is " + LongPressState)
        InjectMidiEventViaRigManager (SWITCH, MakeControlChangeMessageEx(CCBuffer, 0, ChannelBuffer)) //emulating key up
        If IsBankUp == false Then
            //InjectMidiEventViaRigManager (SWITCH, MakeControlChangeMessageEx(ControlCC, 127, BankUpChannel)) //sending control message
            BlinkFS (500) //blinking LEDs long
            looperCounter = 0
            //Notify("Footswitch Bank 2")
        Else
            //InjectMidiEventViaRigManager (SWITCH, MakeControlChangeMessageEx(ControlCC, 0, BankUpChannel))
            //Notify("Footswitch Bank 1")
            BlinkFS (15) //blinking LEDs short
            looperCounter = 2 // setting looper counter to 2 to send trigger messages to looper
        End
        //IsBankUp = !IsBankUp //changing bank flag
    Else
        StopOneShotRamp(LongPress)
        LongPressState = LPSidle
        //Print("changing state to idle, long press is " + LongPressState)
    End
End
1 Like

Suggestion: it is never necessary to write this

2 Likes

Great input in this thread, thanks! I’ll definitely be experimenting a bit in the coming days.

Yes, that’s the inevitable tradeoff. I’d still like to try it though. Even if the delay makes it too annoying for my main effects widgets, I believe it can still be useful for some actions that require less precision.

I’ve had MIDI controllers set up this way in the past (before I moved away from hardware units), and I never really noticed the delay then. We’ll see how it goes.

I would suspect most (or all) devices that use long press do that on button up, so yes, that certainly works.

1 Like

Would there even be another way of doing it? The controller can’t send the short press message on initial press, because it doesn’t know how long the press will last yet. It has to be on release, I would think.

I don’t think so. I did it differently, but it wouldn’t work as a universal solution.

1 Like

One note of caution. If you want to write a gig script that would intercept incoming midi, keep in mind that your controller needs to be connected before you start GP. If you disconnect and then reconnect it while GP is running, the gig script will stop processing midi coming from it. Alternatively you can recompile the gig script after reconnection and it will start working again. It’s some issue with GP, I spent days trying to understand why my script wasn’t working sometimes.

2 Likes

A valuable note of caution, thanks! That actually scares me off the whole process a bit, since there’s always a risk of momentary disconnection in a live setting. Having to deal with a potential restart GP or recompile a script during a set is definitely something I want to avoid at any cost

I hope that issue is solved soon. And it’s just the gig script that stops processing midi, not GP itself. So your long press stuff will stop working, but main functionality will be there.

1 Like

Does the callback

On DeviceStatusChanged(deviceName : String, connected : boolean, reserved integer)

not work to detect device connection/reconnection?

I do all my stuff through the external API, and through that I’ve never experienced disconnects and reconnects being a problem. Although that’s not something I regularly have tested.

It does. But the gig script stops intercepting midi.

You might not notice the problem. I only realized it’s there because I use a Bluetooth footswitch, and reconnection is a frequent occurrence.

And we have indicated to users that bluetooth is not a reliable mechanism…

…for precisely that reason.

Obviously

But since I’m not gigging now it works for me. The issue however exists with all controllers, not just Bluetooth, and they can be reconnected as well, although not as frequently.

Indeed, but the MTBF is orders of magnitude greater when controllers are connected with cables.