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