From 7f34a6c862fc44169df96ff73b23402315daa629 Mon Sep 17 00:00:00 2001 From: Aurelius Prochazka Date: Thu, 15 Feb 2024 16:32:31 -0800 Subject: [PATCH] Made "getRankedChords" more robust to extensions, and made the sorting sensible --- Sources/Tonic/Chord.swift | 48 +++++++++++++++++++++++++------ Tests/TonicTests/ChordTests.swift | 34 ++++++++++++++++++++++ 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/Sources/Tonic/Chord.swift b/Sources/Tonic/Chord.swift index e993120..ccfcdf5 100644 --- a/Sources/Tonic/Chord.swift +++ b/Sources/Tonic/Chord.swift @@ -128,28 +128,58 @@ extension Chord: CustomStringConvertible { } extension Chord { - - /// Get chords that match a set of pitches, listing enharmonic chords with sharp before flats + + public var accidentalCount: Int { + var count = 0 + for note in self.noteClasses { + switch note.accidental { + case .natural: + break + case .flat, .sharp: + count += 1 + case .doubleFlat, .doubleSharp: + count += 2 + } + } + 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 flatNotes: [Note] = [] - var sharpNotes: [Note] = [] + var cNotes: [Note] = [] + var bNotes: [Note] = [] + var sNotes: [Note] = [] var returnArray: [Chord] = [] for pitch in pitchSet.array { - flatNotes.append(Note(pitch: pitch, key: .F)) - sharpNotes.append(Note(pitch: pitch, key: .C)) + cNotes.append(Note(pitch: pitch, key: .C)) + bNotes.append(Note(pitch: pitch, key: .Cb)) + sNotes.append(Note(pitch: pitch, key: .Cs)) } - returnArray.append(contentsOf: Chord.getRankedChords(from: sharpNotes)) + returnArray.append(contentsOf: Chord.getRankedChords(from: cNotes)) - for chord in Chord.getRankedChords(from: flatNotes) { + for chord in Chord.getRankedChords(from: sNotes) { + if !returnArray.contains(chord) { + returnArray.append(chord) + } + } + for chord in Chord.getRankedChords(from: bNotes) { if !returnArray.contains(chord) { returnArray.append(chord) } - } + } + for chord in returnArray { + print(chord, chord.accidentalCount) + } + // order the array by least number of accidentals + returnArray.sort { $0.accidentalCount < $1.accidentalCount } + 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)) let orderedNotes = notes.sorted(by: { f, s in f.noteNumber < s.noteNumber }) diff --git a/Tests/TonicTests/ChordTests.swift b/Tests/TonicTests/ChordTests.swift index 75bc52b..f0b07e2 100644 --- a/Tests/TonicTests/ChordTests.swift +++ b/Tests/TonicTests/ChordTests.swift @@ -260,6 +260,40 @@ class ChordTests: XCTestCase { ) } + func assertChords(_ notes: [Int8], _ expected: [Chord]) { + let pitchSet = PitchSet(pitches: notes.map { Pitch($0) }) + // print(pitchSet.array.map { Note(pitch: $0)}) + let chords = Chord.getRankedChords(from: pitchSet) + // print(chords, expected) + // Note that this is strange that we can't compare the arrays directly + XCTAssertEqual(chords.description, expected.description) + } + + func testDiatonicChords() { + // Basic triads + assertChords([2, 6, 9], [.D]) + + // We prioritize by the number of accidentals + assertChords([1, 5, 8], [.Db, .Cs]) + + // This test shows that we are aware that A# Major triad is more compactly described as Bb + // because of the required C## in the A# spelling + assertChords([10, 14, 17], [.Bb]) + // F should not be reported as E# + assertChords([5, 9, 12], [.F]) + // E could be reported as Fb, but its accidental is lower it is first + assertChords([4, 8, 11], [.E, .Fb]) + // C should not be reported as B# + assertChords([0, 4, 7], [.C]) + // B could be reported as Cb, but its accidental is lower it is first + assertChords([11, 15, 18], [.B, .Cb]) + + // Extensions that can be spelled only without double accidentals should be found + assertChords([1, 5, 8, 11], [Chord(.Cs, type: .dominantSeventh), Chord(.Db, type: .dominantSeventh)]) + assertChords([1, 5, 8, 11, 14], [Chord(.Cs, type: .flatNinth)]) + + } + func testClosedVoicing() { let openNotes: [Int8] = [60, 64 + 12, 67 + 24, 60 + 24, 64 + 36] let results: [Int8] = [60, 64, 67]