3
\$\begingroup\$

I asked this question on Stack Overflow and was directed here. I'm working on a function that will help me quickly find all the upper structure triads (3-note chord) that I can add to a 4-note 7th chord to create a larger compound chord, as well of the roots and names of each triad. The example I'm testing right now is a 13#11 chord, which has the following degrees in a 12-note octave (when the root is 0): [0, 2, 4, 6, 7, 9, 10]*

*side note: [0, 4, 6, 7, 9, 10] would also match as a 13#11.

The base 7th chord is a dominant 7th: [0, 4, 7, 10].I already know which triads complete the chord: a major triad on the 9th, [2, 6, 9], and a diminished triad on the #11, 0, 6, 9 (only the 6 (#11th) and 9 (13th) are actually necessary to build a 13#11 chord; the 2 (9th) is optional).

I actually already have a function that will give me these results, I'm just wondering if there's a faster/more efficient way to do it? It just feels a little bulky/clunky right now.

On Stack Overflow @RobNapier suggested that I look at using Enums instead of String, [String], Int, and [Int]. I definitely get how Enums would help for my triadQualities array. I'm not sure what they would improve with the number combinations. Or what other improvements (enum-related or other) I can make. Can anyone help enlighten me? Any help would be appreciated.

the code

extension Int {
    func degreeInOctave() -> Int {
        switch self {
        case 0...11:
            return self
        case 12...:
            return self - 12
        default:
            return self + 12
        }
    }
}

var ust: [Int] = [0, 2, 4, 6, 7, 9, 10]

let maj = [0, 4, 7]
let min = [0, 3, 7]
let aug = [0, 4, 8]
let dim = [0, 3, 6]
let sus4 = [0, 5, 7]
let sus2 = [0, 2, 7]
let triadDegs = [maj, min, aug, dim, sus4, sus2]

var triadRoots: [Int] = []
var triadQualities: [String] = []

func findUpperStructureTriads(degs: [Int]) {
    let degCount = degs.count

    var firstIndex = 0

    while firstIndex < (degCount - 2) {
        var secondIndex = firstIndex + 1

        while secondIndex < (degCount - 1) {
            var thirdIndex = secondIndex + 1

            while thirdIndex < (degCount) {
                var threeNoteGroup = [degs[firstIndex], degs[secondIndex], degs[thirdIndex]]

                func checkForTriad(triad: [Int]) -> Bool {
                    if triadDegs.contains(triad) {
                        switch triad {
                        case maj:
                            triadQualities.append("major")
                        case min:
                            triadQualities.append("minor")
                        case aug:
                            triadQualities.append("augmented")
                        case dim:
                            triadQualities.append("diminished")
                        case sus4:
                            triadQualities.append("sus4")
                        case sus2:
                            triadQualities.append("sus2")
                        default:
                            ()
                        }
                        return true
                    } else {
                        return false
                    }
                }
                if threeNoteGroup.contains(6), threeNoteGroup.contains(9){
                    var inversionCount = 0
                    var newGroup = threeNoteGroup.map {$0 - threeNoteGroup[0]}

                    while inversionCount < 3 {

                        func invert() {
                            newGroup = newGroup.map {($0 - newGroup[1]).degreeInOctave()}
                            let newlast = newGroup.remove(at: 0)
                            newGroup.append(newlast)
                        }
                        if checkForTriad(triad: newGroup) {
                            print(threeNoteGroup, threeNoteGroup[inversionCount])
                            triadRoots.append(threeNoteGroup[inversionCount])
                            break
                        }
                        invert()
                        inversionCount += 1
                    }
                }
                thirdIndex += 1
            }
            secondIndex += 1
        }
        firstIndex += 1
    }

    for i in 0...(triadRoots.count - 1) {
        print(triadRoots[i], triadQualities[i])
    }
}

findUpperStructureTriads(degs: ust)

outputs

[0, 6, 9] 6
[2, 6, 9] 2
6 diminished
2 major

clarifications

The combined ust chord is always sorted from lowest to highest. Its elements can range between 0 and 11, and they never duplicate. The lower 4-note chord and upper 3-note chord get merged into one larger chord, lower chord first. If any number in the upper chord is already in the combined chord it doesn't get added twice. So if the lower chord is [0, 4, 7, 10] and the upper chord is [2, 7, 10], ust would be [0, 2, 4, 7, 10]. And for that chord the outputs of the combinations function (ideally) would be something like:

[0, 2, 7] 0
[2, 7, 10] 2
0 sus2
7 minor

Note: One thing I realized is that this function may not actually help my larger program—it would replace a function I'm already using that takes hard-coded Int arrays for every combined chord degree array and translates them into string formulas (i.e. [1, 2] = "min triad on the Maj 2nd"). So I've already done the work. And for the new function to work I'd need to figure out an if statement for which degrees are required for completion that changed for every possible chord quality. So this may all be moot

\$\endgroup\$
  • \$\begingroup\$ @MartinR the program works as intended currently—the issue with ust = [0, 2, 4, 7, 10] not working is fixable by changing this if statement if threeNoteGroup.contains(6), threeNoteGroup.contains(9). But I'm not asking about that part (because I know how to fix it). I'm more asking whether the way I'm iterating through the combinations can be improved to be faster and/or take up fewer lines of code. \$\endgroup\$ – Jacob Smolowe May 14 at 2:15
2
\$\begingroup\$

Disclaimer: I am not an expert in music theory, and english is not my first language. Therefore I may use the wrong terms in the following review.

Review of your current code

I/O should be separated from the computation. In your case, the findUpperStructureTriads() function should return something instead of printing it. That makes the function better usable and testable, and increases the clarity of the program.

Also global variables (triadRoots and triadQualities) should be avoided: Calling findUpperStructureTriads() twice will show unexpected results.

The nested loops are better and simpler written as for-loops over (half-open) ranges:

for firstIndex in 0 ..< degs.count {
    for secondIndex in firstIndex+1 ..< degs.count {
        for thirdIndex in secondIndex+1 ..< degs.count {
            // ...
        }
    }
}

and similarly

for inversionCount in 0 ..< 2 { ... }

and

for i in 0..<triadRoots.count { ... }

Note that your version

for i in 0...(triadRoots.count - 1) { ... }

would crash with a “Fatal error: Can't form Range with upperBound < lowerBound” if triadRoots.count is zero.

Use let for variables which are never mutated, e.g.

let threeNoteGroup = [degs[firstIndex], degs[secondIndex], degs[thirdIndex]]

The default case in checkForTriad() is a case which “should not occur” – unless you made a programming error. To detect such an error early, you can use

default:
    fatalError("Should never come here")

But actually function can be replaced by a more efficient dictionary lookup:

let qualities: [[Int]: String] = [
    maj : "major",
    min : "minor",
    dim : "diminished",
]

// ...

if let quality = qualities[newGroup] {
    print(threeNoteGroup, threeNoteGroup[inversionCount])
    triadRoots.append(threeNoteGroup[inversionCount])
    triadQualities.append(quality)
    break
}

Reducing an integer note to an octave can be done with modulo arithmetic:

extension Int {
    var degreeInOctave: Int {
        let mod12 = self % 12
        return mod12 >= 0 ? mod12 : mod12 + 12
    }
}

An alternative approach

Your program tests each subset of three notes of the given degrees, and its three inversions if it is one of the known triads. It may be more efficient to traverse the given list only once and consider each note as the possible root note of each triad. Then you have only to test if the other notes of the triad are in the list or not.

More structure

Using types gives the objects we operate on a name, allows to group the functionality with the objects, provide initialization methods, etc.

For example, instead of a plain array of notes (or is it degrees?) we can define a Chord structure:

struct Chord {
    let notes: [Int] // Increasing array of degrees in the range 0...11

    init(notes: [Int]) {
        // Reduce module 12 and sort:
        self.notes = notes.map { $0.degreeInOctave }.sorted()
    }

    func translated(by offset: Int) -> Chord {
        return Chord(notes: notes.map { $0 + offset })
    }
}

The init method ensures that the numbers are sorted in increasing order and in the proper range. The translated(by:) method computes a new chord by shifting all degrees. More methods can be added later if needed, e.g.

struct Chord {
    mutating func invert() { ... }
}

for chord inversion.

Triads could be defined as an enumeration:

enum Triad: String, CaseIterable {
    case major
    case minor
    case augmented
    case diminished
    case sus4
    case sus2

    var chord: Chord {
        switch self {
        case .major:      return Chord(notes: [ 0, 4, 7 ])
        case .minor:      return Chord(notes: [ 0, 3, 7 ])
        case .augmented:  return Chord(notes: [ 0, 4, 8 ])
        case .diminished: return Chord(notes: [ 0, 3, 6 ])
        case .sus4:       return Chord(notes: [ 0, 5, 7 ])
        case .sus2:       return Chord(notes: [ 0, 2, 7 ])
        }
    }
}

Instead of global variables (maj, min, ...) we can now refer to a triad as values of the enumeration, e.g.

let triad = Triad.major
print(triad)            // major
print(triad.chord)      // Chord(notes: [0, 4, 7])

and with the conformance to CaseIterable we get the list of all triads for free:

for triad in Triad.allCases { ... }

Finally we need a type for the results, which are triads at a certain position, for example:

struct UpperStructure: CustomStringConvertible {
    let triad: Triad
    let root: Int

    var description: String {
        return "\(root) \(triad.rawValue)"
    }
}

The description method provides the textual representation of the values, and can be adjusted to your needs.

With these preparations, we can define a function to find all upper structure triads in a given chord. This can be a method of the Chord type instead of a global function, so that we now have

struct Chord {
    let notes: [Int]

    init(notes: [Int]) {
        self.notes = notes.map { $0.degreeInOctave }.sorted()
    }

    func translated(by offset: Int) -> Chord {
        return Chord(notes: notes.map { $0 + offset })
    }

    func upperStructureTriads() -> [UpperStructure] {
        let notesSet = Set(notes)
        var result: [UpperStructure] = []

        for rootNote in notes {
            for triad in Triad.allCases {
                let chordNotes = triad.chord.translated(by: rootNote).notes
                if chordNotes.contains(6) && chordNotes.contains(9)
                    && notesSet.isSuperset(of: chordNotes) {
                    result.append(UpperStructure(triad: triad, root: rootNote))
                }
            }
        }

        return result
    }
}

All possible combinations of root notes and triads are tested, and a Set is used to make the containment test efficient.

Usage example:

let chord = Chord(notes: [0, 2, 4, 6, 7, 9, 10])
let upperStructures = chord.upperStructureTriads()
for us in upperStructures {
    print(us)
}

// 2 major
// 6 diminished
\$\endgroup\$
  • 1
    \$\begingroup\$ No upper structures are found in these notes [0, 2, 4, 7, 10], or am I mistaken? \$\endgroup\$ – ielyamani May 14 at 17:09
  • 1
    \$\begingroup\$ @ielyamani:Yes (as in OP's code), because it does not contain 6 or 9. See OP's comment at the question. \$\endgroup\$ – Martin R May 14 at 17:11
  • 1
    \$\begingroup\$ In this case, shouldn't the upper structures contain 2 and 7 instead? Just like in the expected result? \$\endgroup\$ – ielyamani May 14 at 17:12
  • 1
    \$\begingroup\$ @ielyamani: I have implemented the same logic as in the original code, which tests if threeNoteGroup.contains(6), threeNoteGroup.contains(9) and therefore does not find upper structures in [0, 2, 4, 7, 10]. \$\endgroup\$ – Martin R May 14 at 17:16
  • 1
    \$\begingroup\$ @JacobSmolowe: You are welcome! If I ever happen to be in SF you can give me a free piano lesson :) \$\endgroup\$ – Martin R May 15 at 20:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.