// --- G P S C R I P T : DIATONIC CHORD PLAYER --- // This script automatically plays the diatonic chords (Triad, 7th, 9th, 11th) // of a selected key and scale, triggered by the input notes. // REQUIRES: // 1. A Key Selector Widget (12 positions, value 0-11) // 2. A Scale Selector Widget (9 positions, value 0-8) // 3. A Chord Extension Selector Widget (4 positions, value 0-3) // 4. A Transpose Widget (Integer or Knob, value -12 to 12) // ==================================================================== // 1. WIDGET AND GLOBAL VARIABLE DECLARATIONS // ==================================================================== // Declare Widgets var // A) Key Selector (0 = C, 11 = B) KeySelector : Widget // B) Chord Extension Selector (0=Triad, 1=7th, 2=9th, 3=11th) ChordExtensionSelector : Widget // C) Scale Selector (0=Major, 1=Minor, etc.) ScaleSelector : Widget // D) Transpose Scale (Integer widget, e.g., -12 to 12) TransposeWidget : Widget // Global State Variables CurrentKey : Integer = 0 // C Major by default CurrentScaleIndex : Integer = 0 // Major by default CurrentExtensionIndex : Integer = 0 // Triad by default CurrentTranspose : Integer = 0 // Array to store the last chord played for Note Off purposes heldChordNotes : Integer array = [0, 0, 0, 0, 0, 0] // Up to 6 notes (11th chord) // Constants and Arrays for scale and note names KEY_NAMES : String array = [ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" ] SCALE_NAMES : String array = [ "Major (Ionian)", "Minor (Aeolian)", "Dorian", "Phrygian", "Lydian", "Mixolydian", "Locrian", "Harmonic Minor", "Melodic Minor" ] EXTENSION_NAMES : String array = [ "Triad (1-3-5)", "7th (1-3-5-7)", "9th (1-3-5-7-9)", "11th (1-3-5-7-9-11)" ] // SCALE DEFINITION (Intervals from the root, in semitones) // The arrays contain the semitone offsets for the 8 notes of the scale (0 to 12). // [1st, 2nd, 3rd, 4th, 5th, 6th, 7th, 8th] const // All 9 scale definitions (as a 2D array is complicated in GPScript, // we use a single large array for simplicity, accessing using index * 8 + degree) ScaleIntervals : Integer array = [ // 0: Major (Ionian) 0, 2, 4, 5, 7, 9, 11, 12, // 1: Natural Minor (Aeolian) 0, 2, 3, 5, 7, 8, 10, 12, // 2: Dorian 0, 2, 3, 5, 7, 9, 10, 12, // 3: Phrygian 0, 1, 3, 5, 7, 8, 10, 12, // 4: Lydian 0, 2, 4, 6, 7, 9, 11, 12, // 5: Mixolydian 0, 2, 4, 5, 7, 9, 10, 12, // 6: Locrian 0, 1, 3, 5, 6, 8, 10, 12, // 7: Harmonic Minor 0, 2, 3, 5, 7, 8, 11, 12, // 8: Melodic Minor 0, 2, 3, 5, 7, 9, 11, 12 ] // Diatonic Chord Degrees (relative to the root of the chord): // These are the *scale degrees* to stack for each extension (1, 3, 5, 7, 9, 11) // Note: Degrees are 1-based (1 to 7). The 9th is the 2nd degree + octave, 11th is 4th + octave. const // [1st, 3rd, 5th, 7th, 9th, 11th] CHORD_DEGREES : Integer array = [1, 3, 5, 7, 2, 4] // Max number of notes for each extension const MAX_NOTES_BY_EXTENSION : Integer array = [3, 4, 5, 6] // Triad, 7th, 9th, 11th // ==================================================================== // 2. WIDGET CHANGE HANDLERS (Live control) // ==================================================================== // A) Update Key and label On WidgetValueChanged(newValue : double) from KeySelector CurrentKey = Round(newValue) SetWidgetLabel(KeySelector, KEY_NAMES[CurrentKey]) End // C) Update Scale and label On WidgetValueChanged(newValue : double) from ScaleSelector CurrentScaleIndex = Round(newValue) SetWidgetLabel(ScaleSelector, SCALE_NAMES[CurrentScaleIndex]) End // B) Update Chord Extension and label On WidgetValueChanged(newValue : double) from ChordExtensionSelector CurrentExtensionIndex = Round(newValue) SetWidgetLabel(ChordExtensionSelector, EXTENSION_NAMES[CurrentExtensionIndex]) End // D) Update Transpose On WidgetValueChanged(newValue : double) from TransposeWidget CurrentTranspose = Round(newValue) End // ==================================================================== // 3. CORE NOTE PROCESSING LOGIC // ==================================================================== // Helper function to turn off the previously held chord Function TurnOffPreviousChord(vel : Integer) var i : Integer for i = 0 to Size(heldChordNotes) - 1 do if heldChordNotes[i] > 0 then SendNoteOffNow(heldChordNotes[i], vel) end // Reset the note in the held list heldChordNotes[i] = 0 end End // Helper function to get the semitone interval of a scale degree. // Scale degrees are 1-based (1st, 2nd, 3rd... up to 11th is 7 + 4 = 11 degrees total) // The ScaleIntervals array only holds 8 notes (1st to 8th) Function GetScaleSemitoneInterval(scaleIndex : Integer, degree : Integer) : Integer // Ensure degree is within bounds (1 to 7/8 in the base octave) // We use the full scale definition (8 notes) var baseIndex : Integer = scaleIndex * 8 var octave : Integer = (degree - 1) div 7 var degreeInOctave : Integer = (degree - 1) mod 7 // The scale intervals array is 0-based for its index, so we access index degreeInOctave + 1 // e.g. 1st degree (0 mod 7) is index 0. 8th degree is index 7 (12 semitones). var intervalOffset : Integer = ScaleIntervals[baseIndex + degreeInOctave] // Add octave shift return intervalOffset + (octave * 12) End // Main Note Handler On NoteEvent(n : NoteMessage) var inputNote : Integer = n.Note inputVelocity : Integer = n.Velocity i : Integer // The scale index offset in the ScaleIntervals array scaleRoot : Integer = CurrentKey // Only process Note On messages if n.IsNoteOn then // 1. Turn off the previous chord before calculating the new one TurnOffPreviousChord(inputVelocity) // 2. Identify the input note's scale degree (1st, 2nd, 3rd, 4th, 5th, 6th, or 7th) // We look for the diatonic note that matches the input note. var diatonicDegree : Integer = -1 // 1-based degree (1 to 7) noteClass : Integer = inputNote mod 12 // C=0 to B=11 scaleInterval : Integer // interval from the scale root // Loop through the 7 scale degrees (1st to 7th) for i = 0 to 6 do // Get the semitone interval for this degree in the current scale // Degree is 1-based, so i + 1 scaleInterval = ScaleIntervals[CurrentScaleIndex * 8 + i] // Calculate the chromatic note class for this degree if noteClass = (scaleRoot + scaleInterval) mod 12 then diatonicDegree = i + 1 // Found the degree (1-based) break // Stop searching end end // 3. If the input note is NOT in the selected scale, ignore it. if diatonicDegree = -1 then // For now, we just pass the note through if it's not diatonic // You can change this to 'SwallowEvent()' to completely filter non-diatonic notes PropagateEvent(n) return end // 4. Calculate the chord based on the diatonic degree var numNotes : Integer = MAX_NOTES_BY_EXTENSION[CurrentExtensionIndex] degreeOffset : Integer chordDegree : Integer semitoneOffset : Integer chordNote : Integer j : Integer = 0 // Index for heldChordNotes // Loop through the required number of notes (3 for Triad, up to 6 for 11th) for i = 0 to numNotes - 1 do // The degree of the chord being built: 1st, 3rd, 5th, 7th, 9th, 11th // These correspond to the CHORD_DEGREES array chordDegree = CHORD_DEGREES[i] // The actual scale degree we are looking for: // e.g., if input is 1st degree (C in C Major), and we want the 3rd of the chord (E), // then the target degree is 1 (input degree) + 2 (interval to 3rd) = 3rd degree. degreeOffset = diatonicDegree + (chordDegree - 1) // 5. Get the semitone offset for the target degree from the MAIN KEY ROOT (CurrentKey) // The semitone offset is calculated using the scale intervals array: semitoneOffset = GetScaleSemitoneInterval(CurrentScaleIndex, degreeOffset) // 6. Calculate the final note number (Root + Semitone Offset + Transpose) // The root octave is determined by the input note's octave var rootOctaveStart : Integer = (inputNote div 12) * 12 + scaleRoot // We need to adjust the chord root to the correct octave based on the input note. // Simplified approach: find the lowest possible note for the chord root (diatonicDegree) // in the vicinity of the input note. // Let's use the input note's MIDI note number as the 1st of the chord for simplicity, // unless the input note is higher than the root of the scale degree. // For diatonic chords, the input note *is* the root of the chord (1st degree). // chordNote = inputNote + (semitoneOffset - (inputNote - scaleRoot) mod 12) + CurrentTranspose // Simplified: Chord root is Input Note + Offset chordNote = inputNote + (semitoneOffset - (inputNote - scaleRoot) mod 12) + CurrentTranspose // A more robust way (which we must use for stability): // The lowest note of the chord (the chord root, or 1st degree) is `inputNote`. // The relative interval we need to add is `semitoneOffset` relative to the // 1st degree of the chord (which is `inputNote`). // Since the input note *is* the root of the chord (e.g., C -> Cmaj, D -> Dmin), // the required intervals are the semitone differences from the *root* of the chord, // which is the `diatonicDegree`'s interval. // 1. Find the chromatic note number of the 1st, 3rd, 5th, etc. in the scale. // 2. Transpose them to the octave of the input note. // Let's re-run the calculation of the chord note interval relative to the CHORD ROOT // The chord root is `inputNote`. We need the interval of the 3rd, 5th, 7th, etc. var baseDegreeOffset : Integer = (diatonicDegree - 1) * 8 var rootInterval : Integer = ScaleIntervals[CurrentScaleIndex * 8 + (diatonicDegree - 1)] var noteInterval : Integer // The semitone difference from the root of the chord to the note we want to play // e.g., Cmaj: C is 1st (0), E is 3rd (4), G is 5th (7). // To get the 3rd: We need the semitone difference between the 3rd degree of the scale // (e.g., E) and the 1st degree of the scale (e.g., C). // Target degree in the full scale (1 to 11) var targetScaleDegree : Integer = (diatonicDegree - 1) + (CHORD_DEGREES[i] - 1) // Get the semitone interval of the target degree relative to the scale root var targetSemitoneOffset : Integer = GetScaleSemitoneInterval(CurrentScaleIndex, targetScaleDegree + 1) // Get the semitone interval of the chord root relative to the scale root var chordRootSemitoneOffset : Integer = GetScaleSemitoneInterval(CurrentScaleIndex, diatonicDegree) // The interval of the note *relative to the chord root* noteInterval = targetSemitoneOffset - chordRootSemitoneOffset // Correct the octave: If the interval is negative (e.g., 9th degree interval - 1st degree interval) // then we need to add 12 to bring it to the next octave. // Note: This logic for 9th/11th is complex. We must ensure `GetScaleSemitoneInterval` // already handles octaves correctly for degrees > 7 (which it does, e.g., 9th=2nd+12). // The final MIDI note to play: chordNote = inputNote + noteInterval + CurrentTranspose // 7. Play the note and store it SendNoteOnNow(chordNote, inputVelocity) heldChordNotes[j] = chordNote j = j + 1 end // Process Note Off messages else // 1. Turn off all notes of the chord. // We only do this on the first NoteOff received, assuming the chord root is the key being released. TurnOffPreviousChord(inputVelocity) // 2. Important: Swallow the event. The script has handled the Note Off for the chord. SwallowEvent() end End // ==================================================================== // 4. INITIALIZATION // ==================================================================== On Activate // Initialize widget values and labels to match default state (C Major, Triad) // Key Selector (C) SetWidgetValue(KeySelector, CurrentKey) SetWidgetLabel(KeySelector, KEY_NAMES[CurrentKey]) // Scale Selector (Major) SetWidgetValue(ScaleSelector, CurrentScaleIndex) SetWidgetLabel(ScaleSelector, SCALE_NAMES[CurrentScaleIndex]) // Chord Extension Selector (Triad) SetWidgetValue(ChordExtensionSelector, CurrentExtensionIndex) SetWidgetLabel(ChordExtensionSelector, EXTENSION_NAMES[CurrentExtensionIndex]) // Transpose (0) SetWidgetValue(TransposeWidget, 0) // Log for debugging Print("Diatonic Chord Player Initialized. Key: " + KEY_NAMES[CurrentKey] + ", Scale: " + SCALE_NAMES[CurrentScaleIndex]) End