Skip to content

Commit

Permalink
Improved getRankedChords algorithm (#43)
Browse files Browse the repository at this point in the history
* WIP minor major ninth not passing yet

* Added Minor Major Ninth chord type

* Added Major and Dominant Ninth Flat Five refactor of Flat Fifth to Flat Five in all cases

@aure

* WIP on new ranked chords algo

Co-authored-by: Aure <aure@aure.com>

* New algo working replacing old one

* A few sus chords do not work due to algo being too eager to find thirds via letter

* remade ranked chord algo with interval strategy

* Fixed bug in Note.noteNumber causing infinite loop

Co-authored-by: Aure <aure@aure.com>

* Added isDouble as computed property to Accidental

* Improved getRankedChords algorithm

Co-authored-by: Aure <aure@aure.com>

* WIP cleaning up chord list refining examples and descriptions

---------

Co-authored-by: Aure <aure@aure.com>
  • Loading branch information
maksutovic and aure authored Apr 20, 2024
1 parent 4b5f3e0 commit 76c665b
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 163 deletions.
9 changes: 9 additions & 0 deletions Sources/Tonic/Accidental.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,12 @@ extension Accidental: Comparable {
lhs.rawValue < rhs.rawValue
}
}

extension Accidental {
var isDouble: Bool {
switch self {
case .doubleFlat, .doubleSharp: return true
default: return false
}
}
}
93 changes: 80 additions & 13 deletions Sources/Tonic/Chord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,22 +182,73 @@ extension Chord {
}
return count
}

/// Get chords that match a set of pitches, ranking by least number of accidentals
public static func getRankedChords(from pitchSet: PitchSet) -> [Chord] {
var noteArrays: Set<[Note]> = []

/// Get chords from a PitchSet, ranked by simplicity of notation
/// - Parameters:
/// - pitchSet: Pitches to be analyzed
/// - allowTheoreticalChords: This algorithim will provide chords with double flats, double sharps, and inergonomic root notes like E# and Cb
public static func getRankedChords(from pitchSet: PitchSet, allowTheoreticalChords: Bool = false) -> [Chord] {
var enharmonicNoteArrays: [[Note]] = []
var returnArray: [Chord] = []

for key in Key.circleOfFifths {
noteArrays.insert(pitchSet.array.map { Note(pitch: $0, key: key) })
for pitch in pitchSet.array {
let octave = pitch.note(in: .C).octave
var noteArray: [Note] = []
for letter in Letter.allCases {
for accidental in Accidental.allCases {
var intValue = Int(letter.baseNote) + Int(accidental.rawValue)
if intValue > 11 {
intValue -= 12
}
if intValue < 0 {
intValue += 12
}
if pitch.midiNoteNumber % 12 == intValue {
noteArray.append(Note(letter, accidental: accidental, octave: octave))
}
}
}
noteArray.sort { n1, n2 in
abs(n1.accidental.rawValue) < abs(n2.accidental.rawValue)
}
enharmonicNoteArrays.append(noteArray)
}
let chordSearchIntervalArray: [[Interval]] =
[[.M3, .m3], [.P5, .d5], [.M7, .m7], [.M9, .m9, .A9], [.P11, .A11], [.M13, .m13, .A13]]

for key in Key.circleOfFourths {
noteArrays.insert(pitchSet.array.map { Note(pitch: $0, key: key) })
var foundNoteArrays: [[Note]] = []
for enharmonicNoteArray in enharmonicNoteArrays {
for rootNote in enharmonicNoteArray {
var usedNoteArrays: [[Note]] = [enharmonicNoteArray]
var foundNotes: [Note] = []
foundNotes.append(rootNote)
for nextIntervals in chordSearchIntervalArray {
var foundNote = false
for nextInterval in nextIntervals {
if foundNote { continue }
guard let searchNoteClass = rootNote.shiftUp(nextInterval)?.noteClass else { continue }
for noteArray in enharmonicNoteArrays where !usedNoteArrays.contains(noteArray) {
if noteArray.map({$0.noteClass}).contains(searchNoteClass) {
guard let matchedNote = noteArray.first(where: {$0.noteClass == searchNoteClass}) else { continue }
foundNotes.append(matchedNote)
usedNoteArrays.append(noteArray)
foundNote = true
}
}
}
if foundNotes.count == pitchSet.count {
foundNoteArrays.append(foundNotes)
}
}
}
}

for noteArray in noteArrays {
returnArray.append(contentsOf: Chord.getRankedChords(from: noteArray))
for foundNoteArray in foundNoteArrays {
let chords = Chord.getRankedChords(from: foundNoteArray)
for chord in chords {
if !returnArray.contains(chord) {
returnArray.append(chord)
}
}
}

// Sorts anti-alphabetical, but the net effect is to pefer flats to sharps
Expand All @@ -211,16 +262,32 @@ extension Chord {

// prefer root notes not being uncommon enharmonics
returnArray.sort { ($0.root.canonicalNote.isUncommonEnharmonic ? 1 : 0) < ($1.root.canonicalNote.isUncommonEnharmonic ? 1 : 0) }



if !allowTheoreticalChords {
returnArray = returnArray.filter { chord in
!chord.root.accidental.isDouble
}
returnArray = returnArray.filter { chord in
!chord.root.canonicalNote.isUncommonEnharmonic
}
returnArray = returnArray.filter { chord in
!chord.bassNote.canonicalNote.isUncommonEnharmonic
}
returnArray = returnArray.filter { chord in
!chord.bassNote.accidental.isDouble
}
}

return returnArray
}

/// Get chords from actual notes (spelling matters, C# F G# will not return a C# major)
/// Use pitch set version of this function for all enharmonic chords
/// The ranking is based on how low the root note of the chord appears, for example we
/// want to list the notes C, E, G, A as C6 if the C is in the bass
public static func getRankedChords(from notes: [Note]) -> [Chord] {
let potentialChords = ChordTable.shared.getAllChordsForNoteSet(NoteSet(notes: notes))
if potentialChords.isEmpty { return [] }
let orderedNotes = notes.sorted(by: { f, s in f.noteNumber < s.noteNumber })
var ranks: [(Int, Chord)] = []
for chord in potentialChords {
Expand Down
Loading

0 comments on commit 76c665b

Please sign in to comment.