Script to automatically play triads in a selected key/mode/inversion

Hi all,

Please find attached my first GPScript project. It is meant for non-keyboard players, like myself, to play a triad (with additional bass note) based on the root note that you play (so 4 notes for the price of one). The plugin is a FabFilter Twin 2, but the script doesn’t depend on it, it can be replaced by any MIDI instrument.

The triads are correctly selected from the key and mode that you specify. There is a keyboard drawn with visual feedback of the notes in the selected key and what degree they represent. Have fun.

Known issues:

  • The radio buttons indicating the inversion tend to oscillate (but not always, haven’t found what causes it)
  • The black keys in the drawn keyboard tend to be underneath the white keys upon startup (remedy: open layout designer, drag select across the bottom end of the keys, right-click, send to back).
  • The rotary knobs and sliders are controlled by rotary encoders on my Axiom keyboard. The controller overrides the widgets which leads to jumps
  • The green LEDs are for visual feedback only. If you click one on, it remains on and will only be reset upon a key of mode change.
  • The binary switch that selects major/minor overrides the mode slider, but the mode slider doesn’t override the switch. It is there only for convenience to provide a shortcut to Ionian or Aeolian modes.

Any feedback on my code or solutions to the above problems are welcome.
The next step in development will be to automatically select the inversion that leads to the smallest change in voicing of the previous to the next chord. If you have any other suggestions, please let me know.

auto_triads.gig (82.6 KB)

var 
axiom : MidiInBlock

root_pos, first_inv, second_inv, key, mode, mami_mode, accn, attn, modeName, keyName : Widget
L0, L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11 : Widget
d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11 : Widget
notesInKeyLeds     : Widget Array
degreesInKeyLabels : Widget Array

root_active, first_active, second_active : Boolean
PrintDebug : Boolean

selKey, selMode : Integer
v_accn, v_attn, i, inversion : Integer

notesOnArray : Integer[128]
noteOnEvents, inversionEvents, notesInKey : Integer Array

notesInKeyActive : Boolean Array
degreeNames : String Array

Initialization
// While debugging leave these lines in the code
/*   v_accn = 10
       v_attn = 5
   selKey = 0
   selMode = 0
*/ // Up to here
   PrintDebug = false
   root_active = false
   first_active = false
   second_active = false
   For i=0; i<128; i = i+1 Do   // Preset all notes that are sounding simultaneously to 0
        notesOnArray[i] = 0
   End
   notesInKeyActive = [false, false, false, false, false, false, false, false, false, false, false, false]
   degreeNames = ["", "", "", "", "", "", "", "", "", "", "", ""]
   notesInKeyLeds = [L0, L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11]
   degreesInKeyLabels = [d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11]
End

On Activate// Called when rackspace is activated
   SetWidgetValue (root_pos,1) //LED #1 is activated -> No inversion
   v_accn = 10
   v_attn = 5
   selKey = 0
   selMode = 0
End

Function set_inv (r_pos : double, f_inv : double, s_inv : double)
    // Set the widgets to their appropriate settings and select the inversion in 'radio button' style
    // The three inputs are only allowed to have a single 1 among otherwise 0s. 
SetWidgetValue(root_pos, r_pos)
SetWidgetValue(first_inv, f_inv)
SetWidgetValue(second_inv, s_inv)
root_active = r_pos > 0
first_active = f_inv > 0
second_active = s_inv > 0
End

Function modulo (value : Integer, divisor : Integer) returns Integer
    result = value  - Floor(value/divisor)*divisor
End

Function getNotes(noteValue : Integer, inversionQuality : Integer) returns Integer Array
// Select chord notes based on inversion and chord quality (major, minor, diminished)
Select // Order of the array: root, third, fifth, bass=root -2 octaves
    inversionQuality == 0 Do // Root position and major triad
    result = [noteValue, noteValue+4, noteValue+7, noteValue-24]
    inversionQuality == 1 Do // First inversion and major triad
    result = [noteValue, noteValue+4, noteValue-5, noteValue-24]
    inversionQuality == 2 Do // Second inversion and major triad
    result = [noteValue, noteValue-8, noteValue-5, noteValue-24]
    inversionQuality == 3 Do // Root position and minor triad
    result = [noteValue, noteValue+3, noteValue+7, noteValue-24]
    inversionQuality == 4 Do // First inversion and minor triad
    result = [noteValue, noteValue+3, noteValue-5, noteValue-24]
    inversionQuality == 5 Do // Second inversion and minor triad
    result = [noteValue, noteValue-9, noteValue-5, noteValue-24]
    inversionQuality == 6 Do // Root position and diminished triad
    result = [noteValue, noteValue+3, noteValue+6, noteValue-24]
    inversionQuality == 7 Do // First inversion and dinminished triad
    result = [noteValue, noteValue+3, noteValue-6, noteValue-24]
    inversionQuality == 8 Do // Second inversion and diminished triad
    result = [noteValue, noteValue-9, noteValue-6, noteValue-24]
End
End

Function removeElementAtIndex(someArray : Integer Array, index : Integer)
// Removes data at any index from array
var i : Integer
For i = index; i < Size(someArray) - 1; i = i + 1 Do
    someArray[i] = someArray[i+1] // Move remainder data 1 index lower
End
RemoveLast(someArray)
End

Function saturate(inputVal : Integer, minVal : Integer, maxVal : Integer) returns Integer
If inputVal > maxVal
    Then result = maxVal
Elsif inputVal < minVal
    Then result = minVal
Else
    result = inputVal
End
End

Function findNotesInKey(selKey : integer, selMode : integer, notesInKeyActive : Boolean Array, degreeNames : String Array) returns Integer Array
Var i, k, selModeIx : Integer
notes, modeOffsets, degreeOrder : Integer Array
degreeMinor, degreeMajor, degreeDim : String Array

modeOffsets = [0, 2, 4, 5, 7, 9, 11] // Local constant, same as On WidgetValueChanged from key or mode
degreeOrder = [1, 0, 0, 1, 1, 0,  2] // Make sure these degree arrays have the same length as modeOffsets
degreeMinor = ["i",  "ii",  "iii",  "iv",  "v",  "vi",  "vii"]
degreeMajor = ["I",  "II",  "III",  "IV",  "V",  "VI",  "VII"]
degreeDim   = ["io", "iio", "iiio", "ivo", "vo", "vio", "viio"]

selModeIx = IndexOf (modeOffsets, selMode)
For i = 0; i < Size(degreeNames); i=i+1 Do
    degreeNames[i] = ""
    notesInKeyActive[i] = false
End


For i = 0; i < Size(modeOffsets); i = i + 1 Do
// Determine at which note to start in order to follow the Ionian interval order
// and continue from there
notes[i] = modulo(12 + modeOffsets[i] + selKey - selMode, 12)
// Determine degree index of current note to give it the correct degree name
k = modulo(7 + i - selModeIx, 7) 

// Set LEDs status according to the allowed notes in the key
notesInKeyActive[notes[i]] = true

// Determine the corresponding degrees of the scale
If degreeOrder[i] == 0
    Then degreeNames[notes[i]] = degreeMinor[k]
Elsif degreeOrder[i] == 1
    Then degreeNames[notes[i]] = degreeMajor[k]
Elsif degreeOrder[i] == 2
    Then degreeNames[notes[i]] = degreeDim[k]
Else degreeNames[notes[i]] = ""
End
End
Sort(notes, true)

result = notes
End

Function updateDegreesInKeyLabels(degreeNames : String Array)
Var i : Integer

For i = 0; i < Size(degreeNames); i = i + 1 Do
    SetWidgetLabel(degreesInKeyLabels[i], degreeNames[i])
End
End

Function updateNotesInKeyLeds(noteActiveArray : Boolean Array)
Var i : Integer

For i = 0; i < Size(noteActiveArray); i = i + 1 Do
    If noteActiveArray[i] Then
        SetWidgetValue(notesInKeyLeds[i], 1.0)
    Else
        SetWidgetValue(notesInKeyLeds[i], 0.0)
    End
End
End


// Called when a NoteOn or NoteOff message is received at the Axiom MidiIn block
// Based on the setting of the radio buttons, the chord is completed in root position, 
// first inversion or second inversion with an added bass note, two octaves below
// the note played.

On NoteEvent(m : NoteMessage) from axiom
Var n, r, third, fifth, v1, v3, v5, vb, thisInversion, thatInversion, index : integer
noteArray : Integer Array
strNotesOnArray, strNoteOnEvents, strInversionEvents : string
n = GetNoteNumber(m)

//Make sure all generated chord notes fit in MIDI note range (0-127)
// otherwise ignore the NoteMessage
If n > 23 And n < 121 
Then

// Saturate the input velociy such that accentuation of the melody note (highest pitch)
// and attenuation of the bass note (lowest pitch) are possible
v1 = saturate(GetVelocity(m), v_attn, 127-v_accn)

// Determine which degree of the scale it is
// Transpose the note number back to its position in the C major scale
r = modulo(12 + n - selKey + selMode, 12) 
If r == 0 Or r == 5 Or r == 7          // If the transposed note is C, F or G
    Then thisInversion = inversion     // make it a major triad
Elsif r == 11                          // If the transposed note is a B
    Then thisInversion = inversion + 6 // Make it a diminished triad
Else  // If the transposed note is D, E, A or a note outside of the C major scale
     thisInversion = inversion + 3     // Make it a minor triad
End

// Accentuate the highest pitch and attenuate the lowest pitch
If root_active And IsNoteOn(m)
    Then v3 = v1
         v5 = v1+v_accn
         vb = v1-v_attn
 Elsif first_active And IsNoteOn(m)
    Then v3 = v1+v_accn
         v5 = v1
         vb = v1-v_attn
 Elsif second_active And IsNoteOn(m)
    Then v3 = v1
         v5 = v1
         vb = v1-v_attn
         v1 = v1+v_accn
 Else    v1 = 0
         v3 = 0
         v5 = 0
         vb = 0
 End


If IsNoteOn(m)
    Then
    noteArray = getNotes(n, thisInversion)         // Get the notes from chord based on its root and the selected inversion
    AppendInteger(noteOnEvents, n)                 // Remember the note for when it will be released later 
    AppendInteger(inversionEvents, thisInversion)  // And all notes that were added to to the chord (encoded by the currently selected inversion)
    // Update the array in which we keep track of how many times a specific note has been enabled
    notesOnArray[noteArray[0]] = notesOnArray[noteArray[0]] + 1
    notesOnArray[noteArray[1]] = notesOnArray[noteArray[1]] + 1
    notesOnArray[noteArray[2]] = notesOnArray[noteArray[2]] + 1
    notesOnArray[noteArray[3]] = notesOnArray[noteArray[3]] + 1
Elsif IsNoteOff(m)
    Then
    index = IndexOf(noteOnEvents, n)               // Find its place in the events array
    thatInversion = inversionEvents[index]         // Retrieve the corresponding inversion
    removeElementAtIndex(noteOnEvents, index)      // Remove the event to avoid overflow (and ambiguity)
    removeElementAtIndex(inversionEvents, index)
    noteArray = getNotes(n, thatInversion)         // Get the chord notes that were originally played
    // And update the array in which we keep track of how many times a specific note has been enabled
    notesOnArray[noteArray[0]] = If notesOnArray[noteArray[0]]>0 Then notesOnArray[noteArray[0]] - 1 Else 0 End
    notesOnArray[noteArray[1]] = If notesOnArray[noteArray[1]]>0 Then notesOnArray[noteArray[1]] - 1 Else 0 End
    notesOnArray[noteArray[2]] = If notesOnArray[noteArray[2]]>0 Then notesOnArray[noteArray[2]] - 1 Else 0 End
    notesOnArray[noteArray[3]] = If notesOnArray[noteArray[3]]>0 Then notesOnArray[noteArray[3]] - 1 Else 0 End
End

// Print debug information
 If PrintDebug // And IsNoteOn(m)
    Then
    strNotesOnArray = "notesOnArray: "
    strNoteOnEvents = "noteOnEvents: "
    strInversionEvents = "inversionEvents: "
    For i = 0; i < Size(notesOnArray); i = i+1 Do
        If notesOnArray[i] > 0 
            Then
            strNotesOnArray = strNotesOnArray + "(" + IntToString(i) + ", "+ IntToString(notesOnArray[i]) + "), "
        End
    End
    For i = 0; i < Size(noteOnEvents); i = i+1 Do
        strNoteOnEvents = strNoteOnEvents + "(" + IntToString(i) + ", "+ IntToString(noteOnEvents[i]) + "), "
        strInversionEvents = strInversionEvents + "(" + IntToString(i) + ", "+ IntToString(inversionEvents[i]) + "), "
    End
    Print("Key         : " + selKey)
    Print("Mode        : " + selMode)
    Print("Current 3rd : " + IntToString(noteArray[1]-n))
    Print("Current 5th : " + IntToString(noteArray[2]-n))
    Print("Note played : " + n)
    Print("Octave pos. : " + r)
    Print("Played 3rd  : " + noteArray[1])
    Print("Played 5th  : " + noteArray[2])
    Print("Played Bass : " + noteArray[3])
    Print("Velo. Root  : " + v1)
    Print("Velo. 3rd   : " + v3)
    Print("Velo. 5th   : " + v5)
    Print("Velo. Bass  : " + vb)
    Print(strNotesOnArray)
    Print(strNoteOnEvents)
    Print(strInversionEvents)
    Print(" ")
End

// Send the completed Chord as a series of NoteOn/NoteOff messages
If IsNoteOn(m)
    Then
    // Play all notes, even if they are already sounding
    SendNow(axiom, WithVelocity(m, v1)) // Play the root note
    SendLater(axiom, WithNoteNumberAndVelocity(m, noteArray[1], v3), 15) // And the third
    SendLater(axiom, WithNoteNumberAndVelocity(m, noteArray[2], v5), 30) // And the fifth
    SendNow(axiom, WithNoteNumberAndVelocity(m, noteArray[3], vb)) // And the bass note
Elsif IsNoteOff(m)
    Then
    // Release only those notes that are no longer part of any sounding chord
        If notesOnArray[noteArray[0]] == 0
            Then
            SendNow(axiom, WithVelocity(m, v1))
        End
        If notesOnArray[noteArray[1]] == 0
            Then
            SendLater(axiom, WithNoteNumberAndVelocity(m, noteArray[1], v3), 15)
        End
        If notesOnArray[noteArray[2]] == 0
            Then
            SendLater(axiom, WithNoteNumberAndVelocity(m, noteArray[2], v5), 30)
        End
        If notesOnArray[noteArray[3]] == 0
            Then
            SendNow(axiom, WithNoteNumberAndVelocity(m, noteArray[3], vb))
        End
End
End // If n > 23 And n < 121
End




// Called when a widget value has changed
On WidgetValueChanged(newVal : double) from attn
   v_attn = ScaleRange (newVal, 0, 10)
end

On WidgetValueChanged(newVal : double) from accn
   v_accn = ScaleRange (newVal, 0, 10)
end

On WidgetValueChanged(newVal : double) from key
var selectedKey, index : integer
    modeOffsets : Integer Array
    keyNames, keyNames1, keyNames3, keyNames6, keyNames8, keyNames10 :   String Array
    thisKeyName : String

    keyNames   = ["C", "C#/Db", "D", "D#/Eb", "E", "F", "F#/Gb", "G", "G#/Ab", "A", "A#/Bb", "B"]
keyNames1  = ["Db", "C#", "C#", "Db", "C#", "C#", "C#"]
keyNames3  = ["Eb", "Eb", "D#", "Eb", "Eb", "D#", "D#"]
keyNames6  = ["F#", "F#", "F#", "Gb", "F#", "F#", "F#"]
keyNames8  = ["Ab", "G#", "G#", "Ab", "Ab", "G#", "G#"]
keyNames10 = ["Bb", "Bb", "A#", "Bb", "Bb", "Bb", "A#"]
modeOffsets = [0, 2, 4, 5, 7, 9, 11] // Local constant, same as in function findNotesInKey() and WidgetValueChanged from mode

   selectedKey = ScaleRange(newVal, 0, 11)
   If selKey != selectedKey
      Then selKey = selectedKey

        // If it has changed: name this key and find the notes in it (mode dependent)
        index = IndexOf(modeOffsets, selMode)

        If selKey == 1 
            Then thisKeyName = keyNames1[index]
        Elsif selKey == 3 
            Then thisKeyName = keyNames3[index]
        Elsif selKey == 6 
            Then thisKeyName = keyNames6[index]
        Elsif selKey == 8 
            Then thisKeyName = keyNames8[index]
        Elsif selKey == 10 
            Then thisKeyName = keyNames10[index]
        Else  thisKeyName = keyNames[selKey]
        End

        SetWidgetLabel(keyName, thisKeyName)

        notesInKey = findNotesInKey(selKey, selMode, notesInKeyActive, degreeNames)
        updateNotesInKeyLeds(notesInKeyActive)
        updateDegreesInKeyLabels(degreeNames)
End
End

On WidgetValueChanged(setting : double) from mami_mode
// The binary switch can quickly set the mode to Ionian (major) or Aeolean (minor)
// It overrides the mode slider and updates the modeName, and keyboard indicator widgets
   If setting == 0
        Then selMode = 9  // Natural Minor Scale selected (Aeolian mode)
        SetWidgetValue(mode, MidiToParamEx(9, 0, 11, 0.0, 1.0))
        SetWidgetLabel(modeName, "Aeolian (Natural Minor Scale)")
   Else selMode = 0       // Major Scale selected (Ionian mode)
        SetWidgetValue(mode, MidiToParamEx(0, 0, 11, 0.0, 1.0))
        SetWidgetLabel(modeName, "Ionian (Major Scale)")
   End

   // Update the degree names and note indicator LEDs
   notesInKey = findNotesInKey(selKey, selMode, notesInKeyActive, degreeNames)
   updateNotesInKeyLeds(notesInKeyActive)
   updateDegreesInKeyLabels(degreeNames)
End

On WidgetValueChanged(newVal : double) from mode
var selection, selectedMode   : integer
    modeNames   : String Array
    modeOffsets : Integer Array

    selection = ScaleRange (newVal, 0, 6)
    modeOffsets = [0, 2, 4, 5, 7, 9, 11] // Local constant, same as in function     findNotesInKey() and WidgetValueChanged from key

    selectedMode = modeOffsets[selection]
    If selMode != selectedMode
        Then selMode = selectedMode
    
        // If it has changed: name the mode and find the notes in it (key dependent)
        modeNames = ["Ionian (Major Scale)", "Dorian", "Phrygian", "Lydian", "Mixolydian", "Aeolian (Natural Minor Scale)", "Locrian"]

        SetWidgetLabel(modeName, modeNames[selection])

        notesInKey = findNotesInKey(selKey, selMode, notesInKeyActive, degreeNames)
        updateNotesInKeyLeds(notesInKeyActive)
        updateDegreesInKeyLabels(degreeNames)
    End
   // Ionian     : selMode = 0
   // Dorian     : selMode = 2
   // Phrygian   : selMode = 4
   // Lydian     : selMode = 5
   // Mixolydian : selMode = 7
   // Aeolian    : selMode = 9
   // Locrian    : selMode = 11

End

On WidgetValueChanged(setting : double) from root_pos
/*If root_active
    Then Print("Root position: True, New value: " + DoubleToString(setting, 2))
Else
    Print("Root position: False, New value: " + DoubleToString(setting, 2))
    End
*/
    if setting == 1.0 And !root_active then
       set_inv (1,0,0)
       inversion = 0
    end

    if setting == 0.0 And root_active then
        // root_active = true
        SetWidgetValue(root_pos,1)
    end
end

On WidgetValueChanged(setting : double) from first_inv
    if setting == 1.0 And !first_active then
       set_inv (0,1,0)
       inversion = 1
    end

    if setting == 0.0 And first_active then
        // first_active = true
        SetWidgetValue(first_inv,1)
    end
end

On WidgetValueChanged(setting : double) from second_inv
    if setting == 1.0 And !second_active then
       set_inv (0,0,1)
       inversion = 2
    end

    if setting == 0.0 And second_active then
        // second_active = true
        SetWidgetValue(second_inv,1)
    end
end
4 Likes

May I ask you to put the GPScript in the post too to be able to read it even with Discourse and perhaps the corresponding screenshot if it helps to understand?

Are you sure? It’s a bit long.

I forgot about this :grimacing:. My longer GPScript has about 2500 lines and I didn’t post it in clear text. :stuck_out_tongue_closed_eyes:
Of course if it is not possible forget it. It depends what “long” means to you. But it is true that I like reading GPScript even when not on my PC. Now it is breakfast time for me and I love a little GPScript together with my coffee :coffee: :stuck_out_tongue_winking_eye:

1 Like

There you go. Long is >400 lines :crazy_face:

Yeah, you are definitely as crazy as I am. Welcome to the GPScript club :stuck_out_tongue_winking_eye:

Wow — I designed and implemented this language and I’ve never written a script that more than about 40 lines in it. I continue to be amazed by what people are doing with it!

2 Likes