Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved getRankedChords algorithm #43

Merged
merged 12 commits into from
Apr 20, 2024
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switch and Case Statement Alignment Violation: Case statements should vertically align with their enclosing switch statement. (switch_case_alignment)

default: return false
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switch and Case Statement Alignment Violation: Case statements should vertically align with their enclosing switch statement. (switch_case_alignment)

}
}
}
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]> = []

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)

/// 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line Length Violation: Line should be 120 characters or less: currently 147 characters (line_length)

public static func getRankedChords(from pitchSet: PitchSet, allowTheoreticalChords: Bool = false) -> [Chord] {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cyclomatic Complexity Violation: Function should have complexity 10 or less: currently complexity equals 20 (cyclomatic_complexity)
Function Body Length Violation: Function body should span 40 lines or less excluding comments and whitespace: currently spans 57 lines (function_body_length)

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identifier Name Violation: Variable name should be between 3 and 40 characters long: 'n1' (identifier_name)
Identifier Name Violation: Variable name should be between 3 and 40 characters long: 'n2' (identifier_name)

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 }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line Length Violation: Line should be 120 characters or less: currently 131 characters (line_length)

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) }



Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)

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
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)

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
Loading