Expression pedals, sustain pedals, and loudness metering

Dear friends,

I would like to share three ideas I’ve come up with and implemented that quite proud of. I hope these will be useful to others! I welcome your feedback and suggestions of course; perhaps my solutions are needlessly complicated and I’m always open to learning better methods. (These ideas do sound a lot more complicated than they actually are in practice; implementing them took some thought and some experimentation, but once I got them working, they’re so transparent that I barely notice they’re there.)

The first idea is my answer to the question, “how can I use one sustain pedal to control multiple keyboards and multiple keyboard zones?” In my setup, one rackspace can have up to eight different instruments, each one potentially controlled from a different zone on two different keyboards. (In practice it’s usually just two, for example, piano on the lower keyboard, organ on the upper keyboard, but I’ve set up my system to accommodate as many as eight.) Obviously I don’t want an all-or-nothing sustain pedal, because there are situations where I want to sustain a pad on one instrument while playing staccato notes on another.

Here’s what I came up with, and it seems to work very well, with little or no modification needed to my usual playing style. When a sustain-on event arrives, I send it along only to those instruments that have any notes currently pressed. So, for example, I could play a chord on the strings patch, press the sustain pedal while those notes are pressed, and then play something rhythmic on the piano patch with both hands while the strings pad continues to sustain. If a sustain-on event arrives when there are no notes pressend, then I set a “sustain pending” flag, and then subsequently when a note-on event comes, I send the sustain event only to the patches to which that note-on event applies before passing on the note-on event. Thus, I can press the sustain pedal first, then play a chord or note which I want sustained on one keyboard, and then play something unsustained on the othe keyboard. When a sustain-off event arrives, I send it to every patch, rather than bothering to remember which patches got the sustain-on event. As far as I’m aware, this idea is novel. In retrospect it seems obvious, but I’ve never heard anyone else mention it before.

Here’s my second useful idea, which is my answer to the situation where a physical control (such as a slider or expression pedal) starts out at a different value than the widget which it controls. I don’t want “jump” behaviour because I don’t want the widget to jump, and I don’t want “catch” behaviour because then I have to keep track mentally of whether the widget has been “caught” yet, and because sometimes in order to catch it I’ll have to move the physical control in the opposite direction from the actualy desired movement (e.g. if the widget is at 80, and the pedal is at 50, and I want to reduce the widget value, with “catch” I would first have to push the pedal up past 80 in order to catch the widget, before then pulling it back down to the desired level.)

My solution is that when the physical control increases in value, I increase the widget value by an amount proportional to how far the control moved toward its upper extreme, and when the control decreases in value, I decrease the widget value proportionally to how far the control moved toward its lower extreme. To use the above example, if the widget is at 80 and the expression pedal is at 50, then if the player moves the expression pedal up to 75 (in other words, halfway to the top), then my code moves the widget up to 90 (also halfway to the top). If the player subsequently move the pedal down to 25 (two-thirds of the way down from 75 to 0), then my code moves the widget down to 30 (two-thirds of the way down from 90 to 0). It only takes a few pedal movements for the pedal and the widget to be nearly in sync, and even before that’s the case the movements are still logical (moving the control always causes the widget to move in the same direction as the control). To use a more extreme example, say the pedal starts out at zero and the widget starts out at 80. If I want to decrease the widget value, in this case I can’t reduce the pedal value because it’s already at zero, so I have to push the pedal upward, but even if I go halfway up (to 50), the widget will only have increased to 90 (a small and hopefully acceptable increase), and then if I pull the pedal down to 25 (half of 50) it will drag the widget down to 45 (half of 90).

I’ve placed the GPScript code for the above two ideas in my Global Rackspace script, which is also where all of the code for handling keyboard zones and incoming MIDI events lives (currently about 600 lines of code not including comments and whitespace, and still growing…)

My third contribution has to do with gain-staging and managing loudness. I find that the built-in meter widgets are quirky and not very informative; they are useful to show that there is a signal present, and vaguely how loud or quiet it is, but not very useful for detailed loudness management. This is partly due to the lack of a numerical scale (correct me if I’m wrong, but there doesn’t seem to be a way to make a backplate visible), but mostly due to the fact that they seem to be showing the peak levels (which have very little to do with perceived loudness) rather than RMS levels, so if I have a signal with a very reasonable RMS loudness of -10dBFS, the meter will often go right up into the red because it’s showing the peaks rather than the average. (Again, correct me if I’m wrong about this.)

My solution to this is to use a metering plugin that shows EBU values: integrated loudness (over the lifetime of the signal), short-term loudness (showing the last three seconds), and momentary loudness (showing the last 400ms). However, the problem then becomes how to get a widget on the panel to display these values. In the language of GP, I want a plugin that sends its measurements to the host through parameters, but that’s not the language of plugin marketing (aimed at traditional DAW users), so what should I Google? The answer is to find a plugin that can “write automation data”. And, sure enough, a Google search for loudness metering plugins that write automation data led me to “dpMeter5”, which seems to be excellent. I have yet to determine whether its CPU usage will be a problem; I currently have nine instances of the plugin in my Global Rackspace (one on the master bus and one on each of my 8 instruments). If this takes up too much processing power then I’ll add a widget to disable the metering when I don’t specifically need it, but I’m hoping it’s lightweight enough that I can just leave them running all the time.

I look forward to your comments and feedback.

10 Likes

Nice and clever solutions to common problems … would be great if you share a demo gig or used scripts (especially for item 1 and 2)

1 Like

Yes, please equip your post with screenshots, gig files, etc :slight_smile:

These solutions you have created sound great. I love to see how people create scripted solutions to problems that can’t be solved by the controller hardware or the existing plugins alone.

It’s still a work in progress so I’m not sure I can really share a working gig file. Here’s my code for handling the sustain predal; it’s pretty solid, I think:

// This is an excerpt from my mammoth Global Rackspace script. Please
// don't expect it to run as-is, because it's part of a bigger
// system. I think my code is pretty readable, so it should give you an
// idea of how to implement this same scheme within your global
// rackspace. My framework for handling keyboard zones is something I've
// omitted here. I'm sorry I can't share the entire script because yet I
// don't have all the kinks worked out. I will try to share it as soon as
// I think it's presentable.

// This is the passage that handles incoming note events and sustain
// events. Conceptually, in my system there are up to eight "patches",
// numbered 1-8, which are instruments that listen to the Local GP
// Port on the MIDI channel corresponding to their respective patch number, and
// send their audio output into the corrspending pair of channels on a
// mixer. So, the job of the note event handler is to determine if a
// given note event is within the keyboard zone of each patch, and send
// the event out to those patches that it belongs to. The sustain event
// handler keeps track of which patches should receive the sustain event,
// specifically, those patches whose zones currently have notes pressed.

// The function zoneIsActive returns true if the given patch has an
// active keyboard zone on the given keyboard. (In my system, a patch can
// have two active zones, one on each keyboard, for example in the
// situation where I want strings layered with piano on the upper
// keyboard but just piano alone on the lower keyboard).

Function zoneIsActive(patch: Integer, kbd: Integer) returns Boolean
  // ...definition omitted...
End

// These next three functions return the lower and upper bounds of the
// zone on the given keyboard corresponding to the given patch (assuming
// there is one), and a transposition amount (given in octaves) for that
// zone

Function zoneLowNote(patch: Integer, kbd: Integer) returns Integer
  // ...definition omitted...
End

Function zoneHighNote(patch: Integer, kbd: Integer) returns Integer
  // ...definition omitted...
End

Function zoneTransposition(patch: Integer, kbd: Integer) returns Integer
  // ...definition omitted...
End

// my two physical keyboards

var upperKB, lowerKB : MidiInBlock

var
  patchActive_1, patchActive_2, patchActive_3, patchActive_4, patchActive_5, patchActive_6, patchActive_7, patchActive_8,
  sustainPressed_1, sustainPressed_2, sustainPressed_3, sustainPressed_4, sustainPressed_5, sustainPressed_6, sustainPressed_7, sustainPressed_8,
  noteCount_1, noteCount_2, noteCount_3, noteCount_4, noteCount_5, noteCount_6, noteCount_7, noteCount_8 : Widget

// these eight widgets are toggles on the main UI panel allowing the player to turn specific patches on and off

var patchActives : Widget[8] = [ patchActive_1, patchActive_2, patchActive_3, patchActive_4, patchActive_5, patchActive_6, patchActive_7, patchActive_8 ]

// these eight widgets are text labels on the main UI panel which display the current number of notes pressed for each patch

var noteCounts : Widget[8] = [ noteCount_1, noteCount_2, noteCount_3, noteCount_4, noteCount_5, noteCount_6, noteCount_7, noteCount_8 ]

// these eight widgets are indicators on the main UI panel to inform the player of which patches currently have the sustain pedal pressed

var sustainPresseds : Widget[8] = [ sustainPressed_1, sustainPressed_2, sustainPressed_3, sustainPressed_4, sustainPressed_5, sustainPressed_6, sustainPressed_7, sustainPressed_8 ]

// Note tracker object to keep track of which patches have notes pressed

var noteTrackr : MultiChannelNoteTracker

// Flag to indicate that the sustain pedal has been pressed when no notes are currently pressed

var sustainPending : Boolean = false;

// the exchangeKeyboardsSwitch widget is only for my use in
// development and testing, not meant for use in performance. It's just
// for when I only have one keyboard connected but I want to simulate
// sending note events from the missing keyboard.

var exchangeKeyboardsSwitch : Widget // only used for note events, not pedals and buttons

// Function sendNoteToPatches to distribute a note message to all the patches that it belongs in

Function sendNoteToPatches(n: NoteMessage, kbd: Integer) // kbd == 1 for upper keyboard, kbd == 0 for lower keyboard
  var patch: Int

  If GetWidgetValue(exchangeKeyboardsSwitch) > 0.5 Then
    kbd = 1 - kbd
  End

  For patch = 1; patch <= 8; patch = patch + 1 Do
    // Documentation says that BetweenNotes treat bounds as exclusive, but empirically they are inclusive

    If zoneIsActive(patch, kbd) and BetweenNotes(zoneLowNote(patch, kbd), n, zoneHighNote(patch, kbd))
       And ((!IsNoteOn(n) or GetVelocity(n) == 0) || GetWidgetValue(patchActives[patch-1]) > 0.5) Then

      If IsNoteOn(n) and GetVelocity(n) > 0 Then
        If sustainPending Then
          SetWidgetValue(sustainPresseds[patch-1], 1)
          InjectMidiEvent("Local GP Port", MakeControlChangeMessageEx(64, 127, patch))
        End
        MultiChannelNoteTracker_GotNoteOn(noteTrackr, GetNoteNumber(n), patch) // saving untransposed note numbers
      Else
        MultiChannelNoteTracker_GotNoteOff(noteTrackr, GetNoteNumber(n), patch)
      End

      InjectMidiEvent("Local GP Port", WithChannel(WithTranspose(n, 12*zoneTransposition(patch, kbd)), patch))

    End

    SetWidgetLabel(noteCounts[patch-1], MultiChannelNoteTracker_NoteOnCount(noteTrackr, patch))
  End

  sustainPending = false;
End

// Send note events to the patches that deserve them

On NoteEvent (n : NoteMessage) From lowerKB
  sendNoteToPatches(n, 0)
End

On NoteEvent (n : NoteMessage) From upperKB
  sendNoteToPatches(n, 1)
End

// This useful wiget, sustainPolaritySwitch, is for that situation
// where I've powered up my keyboard without the sustain pedal plugged
// in, so its polarity wasn't autodetected on startup. Much easier to hit a
// widget on the screen than to crawl around on my knees trying to flip
// the physical switch on the pedal.

var sustainPolaritySwitch : Widget

Function sendSustainToPatches(s: ControlChangeMessage)
  var patch: Int
  var sentToHowManyPatches : Int = 0;

  sustainPending = false;

  If GetWidgetValue(sustainPolaritySwitch) > 0.5 Then
    s = WithCCValue(s, 127 - GetCCValue(s))
  End

  // TODO: implement pass-thru sustain (where a patch receives all
  // sustain events rather than participating in this scheme) and
  // no-sustain (where a patch should never receive sustain events)

  // if the sustain pedal is released, send it to all patches
  If GetCCValue(s) == 0 Then
    For patch = 1; patch <= 8; patch = patch + 1 Do
      InjectMidiEvent("Local GP Port", WithChannel(s, patch))
      SetWidgetValue(sustainPresseds[patch-1], 0)
    End
  Else // if it's pressed
    For patch = 1; patch <= 8; patch = patch + 1 Do
      // send it only to the patches with currently pressed notes
      If GetWidgetValue(patchActives[patch-1]) > 0.5 and
          MultiChannelNoteTracker_NoteOnCount(noteTrackr, patch) > 0 Then 
        InjectMidiEvent("Local GP Port", WithChannel(s, patch))
        SetWidgetValue(sustainPresseds[patch-1], 1)
        sentToHowManyPatches = sentToHowManyPatches + 1;
      End
    End

    If sentToHowManyPatches == 0 Then
      sustainPending = true; // global variable meaning we need to send a sustain event to the next patch that gets a note-on message
    End
  End
End

// Sustain event handler whose job is to send sustain events from the upper keyboard to the patches that have notes pressed

On ControlChangeEvent (c : ControlChangeMessage) Matching 64 from upperKB
  sendSustainToPatches(c)
End

2 Likes

I created this exact solution years ago using bidule by Plogue, so that I could sustain an organ sound while playing stacatto piano parts using the same keyboard and sustain pedal. I am so excited that you scripted this functionality for Gig Performer! Thank you Solomon :slight_smile:

1 Like

I’m excited (and humbled) to know that I wasn’t the first to come up with this idea. :smiley:

2 Likes