From 76c665b0acd9e98c8eac3b62a6795166b8d71196 Mon Sep 17 00:00:00 2001 From: Maximilian Maksutovic Date: Sat, 20 Apr 2024 09:50:55 -0700 Subject: [PATCH] Improved getRankedChords algorithm (#43) * 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 * 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 * Added isDouble as computed property to Accidental * Improved getRankedChords algorithm Co-authored-by: Aure * WIP cleaning up chord list refining examples and descriptions --------- Co-authored-by: Aure --- Sources/Tonic/Accidental.swift | 9 ++ Sources/Tonic/Chord.swift | 93 ++++++++++++-- Sources/Tonic/ChordType.swift | 197 +++++++++++++++--------------- Sources/Tonic/Note.swift | 42 ++++++- Tests/TonicTests/ChordTests.swift | 83 +++++++------ Tests/TonicTests/KeyTests.swift | 16 +-- 6 files changed, 277 insertions(+), 163 deletions(-) diff --git a/Sources/Tonic/Accidental.swift b/Sources/Tonic/Accidental.swift index f81ae8e..ba70042 100644 --- a/Sources/Tonic/Accidental.swift +++ b/Sources/Tonic/Accidental.swift @@ -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 + } + } +} diff --git a/Sources/Tonic/Chord.swift b/Sources/Tonic/Chord.swift index e370987..c58932a 100644 --- a/Sources/Tonic/Chord.swift +++ b/Sources/Tonic/Chord.swift @@ -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 @@ -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 { diff --git a/Sources/Tonic/ChordType.swift b/Sources/Tonic/ChordType.swift index 4a54149..dbe8b64 100644 --- a/Sources/Tonic/ChordType.swift +++ b/Sources/Tonic/ChordType.swift @@ -5,138 +5,143 @@ import Foundation /// Chord type as defined by a set of intervals from a root note class public enum ChordType: String, CaseIterable, Codable { - /// Major Triad: Major Third, Perfect Fifth + //MARK: - Triads + /// Major Triad: Major Third, Perfect Fifth, e.g. `C` case majorTriad - /// Minor Triad: Minor Third, Perfect Fifth + /// Minor Triad: Minor Third, Perfect Fifth, e.g. `Cm` case minorTriad - /// Diminished Triad: Minor Third, Diminished Fifth + /// Diminished Triad: Minor Third, Diminished Fifth, e.g. `C°` case diminishedTriad - /// Major Flat Five Triad: Major Third, Diminished Fifth + /// Major Flat Five Triad: Major Third, Diminished Fifth, e.g. `C♭5` case flatFiveTriad - /// Augmented Triad: Major Third, Augmented Fifth + /// Augmented Triad: Major Third, Augmented Fifth, e.g. `C⁺` case augmentedTriad - /// Suspended 2 Triad: Major Second, Perfect Fifth + /// Suspended 2 Triad: Major Second, Perfect Fifth, e.g. `Csus2` case suspendedSecondTriad - /// Suspended 4 Triad: Perfect Fourth, Perfect Fifth + /// Suspended 4 Triad: Perfect Fourth, Perfect Fifth, e.g. `Csus4` case suspendedFourthTriad - /// Major Sixth: Major Third, Perfect Fifth, Major Sixth + //MARK: - Sixths + /// Major Sixth: Major Third, Perfect Fifth, Major Sixth, e.g. `C6` case sixth - /// Minor Sixth: Minor Third, Perfect Fifth, Major Sixth + /// Minor Sixth: Minor Third, Perfect Fifth, Major Sixth, e.g. `Cm6` case minorSixth - /// Major Sixth Suspended Second: Major Second, Perfect Fifth, Major Sixth + /// Major Sixth Suspended Second: Major Second, Perfect Fifth, Major Sixth, e.g. `C6sus2` case sixthSuspendedSecond - /// Major Sixth Suspended Fourth: Major Fourth, Perfect Fifth, Major Sixth + /// Major Sixth Suspended Fourth: Major Fourth, Perfect Fifth, Major Sixth, e.g. `C6sus4` case sixthSuspendedFourth - - /// Half Diminished Seventh: Minor Third, Diminished Fifth, Minor Seventh + + //MARK: - Sevenths + /// Major Seventh: Major Third, Perfect Fifth, Major Seventh, e.g. `Cmaj7` + case majorSeventh + + /// Dominant Seventh: Major Third, Perfect Fifth, Minor Seventh, e.g. `C7` + case dominantSeventh + + /// Minor Seventh: Minor Third, Perfect Fifth, Minor Seventh, e.g. `Cmin7` + case minorSeventh + + /// Half Diminished Seventh: Minor Third, Diminished Fifth, Minor Seventh, e.g. `Cø7` case halfDiminishedSeventh - /// Diminished Seventh: Minor Third, Diminished Fifth, Minor Seventh + /// Diminished Seventh: Minor Third, Diminished Fifth, Minor Seventh, e.g. `C°7` case diminishedSeventh - /// Dominant Seventh: Major Third, Perfect Fifth, Minor Seventh - case dominantSeventh - - /// Dominant Seventh Suspendend Second: Major Second, Perfect Fifth, Minor Seventh + /// Dominant Seventh Suspendend Second: Major Second, Perfect Fifth, Minor Seventh, e.g. `C7sus2` case dominantSeventhSuspendedSecond - /// Dominant Seventh Suspendend Fourth: Perfect Fourth, Perfect Fifth, Minor Seventh + /// Dominant Seventh Suspendend Fourth: Perfect Fourth, Perfect Fifth, Minor Seventh, e.g. `C7sus4` case dominantSeventhSuspendedFourth + + /// Augmented Major Seventh: Major Third, Augmented Fifth, Major Seventh, e.g. `C+Maj7` + case augmentedMajorSeventh - /// Major Seventh: Major Third, Perfect Fifth, Major Seventh - case majorSeventh - - /// Minor Seventh: Minor Third, Perfect Fifth, Minor Seventh - case minorSeventh - - /// Minor Major Seventh: Minor Third, Perfect Fifth, Major Seventh + /// Minor Major Seventh: Minor Third, Perfect Fifth, Major Seventh, e.g. `CmMaj7` case minorMajorSeventh - - /// Half Diminished Ninth: Minor Third, Diminished Fifth, Minor Seventh, Minor Ninth + + /// Minor Seventh Flat Five: Major Third, Diminished Fifth, Major Seventh, e.g. `Cmaj7(♭5)` + case majorSeventhFlatFive + + /// Dominant Flat Five: Major Third, Diminished Fifth, Minor Seventh, e.g. `C7(♭5)` + case dominantSeventhFlatFive + + /// Dominant Sharp Five: Major Third, Augmented Fifth, Minor Seventh, e.g. `C7(♯5)` + case dominantSeventhSharpFive + + //MARK: - Ninths + /// Major Ninth: Major Third, Perfect Fifth, Major Seventh, Major Ninth, e.g. `Cmaj9` + case majorNinth + + /// Dominant Ninth: Major Third, Perfect Fifth, Minor Seventh, Major Ninth, e.g. `C9` + case dominantNinth + + /// Minor Ninth: Minor Third, Perfect Fifth, Minor Seventh, Major Ninth, e.g. `Cmin9` + case minorNinth + + /// Half Diminished Ninth: Minor Third, Diminished Fifth, Minor Seventh, Minor Ninth, e.g. `Cø9` case halfDiminishedNinth - /// Dominant Ninth: Major Third, Perfect Fifth, Minor Seventh, Major Ninth - case dominantNinth - - /// Dominant Ninth Suspended Fourth: Perfect Fourth, Perfect Fifth, Major Ninth (Major Second) + /// Dominant Ninth Suspended Fourth: Perfect Fourth, Perfect Fifth, Major Ninth (Major Second), e.g. `C9sus4` case dominantNinthSuspendedFourth - /// Flat Ninth: Major Third, Perfect Fifth, Minor Seventh, Minor Ninth - case flatNinth - - /// Sharp Ninth: Major Third, Perfect Fifth, Minor Seventh, Augmented Ninth - case sharpNinth + /// Flat Ninth: Major Third, Perfect Fifth, Minor Seventh, Minor Ninth, e.g. `C7♭9` + case dominantFlatNinth - /// Major Ninth: Major Third, Perfect Fifth, Major Seventh, Major Ninth - case majorNinth + /// Sharp Ninth: Major Third, Perfect Fifth, Minor Seventh, Augmented Ninth, e.g. `C7♯9` + case dominantSharpNinth - /// Minor Major Ninth: Minor Third, Perfect Fifth, Major Seventh, Major Ninth + /// Minor Major Ninth: Minor Third, Perfect Fifth, Major Seventh, Major Ninth, e.g. `CmMaj9` case minorMajorNinth - /// Minor Ninth: Minor Third, Perfect Fifth, Minor Seventh, Major Ninth - case minorNinth - - /// Minor Flat Ninth: Minor Third, Perfect Fifth, Minor Seventh, Minor Ninth + /// Minor Flat Ninth: Minor Third, Perfect Fifth, Minor Seventh, Minor Ninth, e.g. `Cm7♭9` case minorFlatNinth - /// Major Add Nine: Major Third, Perfect Fifth, Major Ninth + /// Major Add Nine: Major Third, Perfect Fifth, Major Ninth, e.g. `Cadd9` case majorAddNine - /// Minor Add Nine: Minor Third, Perfect Fifth, Major Ninth + /// Minor Add Nine: Minor Third, Perfect Fifth, Major Ninth, e.g. `Cm(add9)` case minorAddNine - /// Six Over Nine: Major Third, Perfect Fifth, Major Sixth, Major Ninth + /// Six Over Nine: Major Third, Perfect Fifth, Major Sixth, Major Ninth, e.g. `C6/9` case sixOverNine + + /// Major Ninth Flat Five: Major Third, Diminished Fifth, Major Seventh, Major Nine, e.g. `Cmaj9(♭5) + case majorNinthFlatFive + + /// Major Ninth Sharp Five: Major Third, Augmented Fifth, Major Seventh, Major Nine + case majorNinthSharpFive + + /// Dominant Ninth Flat Five: Major Third, Diminished Fifth, Minor Seventh, Major Nine + case dominantNinthFlatFive - /// Major Eleventh: Major Third, Perfect Fifth, Major Seventh, Major Ninth, Perfect Eleventh + /// Dominant Ninth Sharp Five: Major Third, Augmented Fifth, Minor Seventh, Major Nine + case dominantNinthSharpFive + + //MARK: - Elevenths + /// Major Eleventh: Major Third, Perfect Fifth, Major Seventh, Major Ninth, Perfect Eleventh, e.g. `Cmaj11` case majorEleventh - /// Dominant Eleventh: Major Third, Perfect Fifth, Minor Seventh, Major Ninth, Perfect Eleventh + /// Dominant Eleventh: Major Third, Perfect Fifth, Minor Seventh, Major Ninth, Perfect Eleventh, e.g. `C11` case dominantEleventh - /// Minor Eleventh: Minor Third, Perfect Fifth, Minor Seventh, Major Ninth, Perfect Eleventh + /// Minor Eleventh: Minor Third, Perfect Fifth, Minor Seventh, Major Ninth, Perfect Eleventh, e.g. `Cm11` case minorEleventh - /// Half Diminished Ninth: Minor Third, Diminished Fifth, Minor Seventh, Minor Ninth, Perfect Eleventh + /// Half Diminished Ninth: Minor Third, Diminished Fifth, Minor Seventh, Minor Ninth, Perfect Eleventh, e.g. `Cø11` case halfDiminishedEleventh - /// Minor Seventh Flat Five: Major Third, Diminished Fifth, Major Seventh - case majorSeventhFlatFive - - /// Major Seventh Sharp Five: Major Third, Augmented Fifth, Major Seventh - case majorSeventhSharpFive - - /// Minor Ninth Flat Five: Major Third, Diminished Fifth, Major Seventh, Major Nine - case majorNinthFlatFive - - /// Major Ninth Sharp Five: Major Third, Augmented Fifth, Major Seventh, Major Nine - case majorNinthSharpFive - - /// Dominant Ninth Flat Five: Major Third, Diminished Fifth, Minor Seventh, Major Nine - case dominantNinthFlatFive - - /// Dominant Ninth Sharp Five: Major Third, Augmented Fifth, Minor Seventh, Major Nine - case dominantNinthSharpFive - /// Major Ninth Sharp Eleventh: Major Third, Perfect Fifth, Major Seventh, Major Ninth, Augmented Eleventh case majorNinthSharpEleventh - /// Dominant Flat Five: Major Third, Diminished Fifth, Minor Seventh - case dominantFlatFive - - /// Dominant Sharp Five: Major Third, Augmented Fifth, Minor Seventh - case dominantSharpFive - /// Dominant Flat Ninth Sharp Eleventh: Major Third, Perfect Fifth, Minor Seventh, Minor Ninth, Augmented Eleventh case dominantFlatNinthSharpEleventh @@ -193,8 +198,8 @@ public enum ChordType: String, CaseIterable, Codable { case .halfDiminishedNinth: return [.m3, .d5, .m7, .m9] case .dominantNinth: return [.M3, .P5, .m7, .M9] case .dominantNinthSuspendedFourth: return [.P4, .P5, .M9] - case .flatNinth: return [.M3, .P5, .m7, .m9] - case .sharpNinth: return [.M3, .P5, .m7, .A9] + case .dominantFlatNinth: return [.M3, .P5, .m7, .m9] + case .dominantSharpNinth: return [.M3, .P5, .m7, .A9] case .majorNinth: return [.M3, .P5, .M7, .M9] case .minorMajorNinth: return [.m3, .P5, .M7, .M9] case .minorFlatNinth: return [.m3, .P5, .m7, .m9] @@ -207,11 +212,11 @@ public enum ChordType: String, CaseIterable, Codable { case .minorEleventh: return [.m3, .P5, .m7, .M9, .P11] case .halfDiminishedEleventh: return [.m3, .d5, .m7, .m9, .P11] case .majorSeventhFlatFive: return [.M3, .d5, .M7] - case .majorSeventhSharpFive: return [.M3, .A5, .M7] + case .augmentedMajorSeventh: return [.M3, .A5, .M7] case .majorNinthSharpEleventh: return [.M3, .P5, .M7, .M9, .A11] case .dominantFlatNinthSharpEleventh: return [.M3, .P5, .m7, .m9, .A11] - case .dominantFlatFive: return [.M3, .d5, .m7] - case .dominantSharpFive: return [.M3, .A5, .m7] + case .dominantSeventhFlatFive: return [.M3, .d5, .m7] + case .dominantSeventhSharpFive: return [.M3, .A5, .m7] case .dominantSharpNinthSharpEleventh: return [.M3, .P5, .m7, .A9, .A11] case .minorSeventhFlatNinthAddEleventh: return [.m3, .P5, .m7, .m9, .P11] case .majorThirteenth: return [.M3, .P5, .M7, .M9, .P11, .M13] @@ -253,27 +258,29 @@ extension ChordType: CustomStringConvertible { case .majorSeventh: return "maj7" case .minorSeventh: return "m7" case .minorMajorSeventh: return "mMaj7" + case .majorSeventhFlatFive: return "maj7(♭5)" + case .augmentedMajorSeventh: return "maj7(♯5)" + case .dominantSeventhFlatFive: return "7♭5" + case .dominantSeventhSharpFive: return "7♯5" case .halfDiminishedNinth: return "ø9" case .dominantNinth: return "9" case .dominantNinthSuspendedFourth: return "9sus4" - case .flatNinth: return "7♭9" - case .sharpNinth: return "7♯9" + case .dominantFlatNinth: return "7♭9" + case .dominantSharpNinth: return "7♯9" case .majorNinth: return "maj9" case .minorFlatNinth: return "m7♭9" case .minorNinth: return "m9" case .minorMajorNinth: return "mMaj9" case .majorAddNine: return "add9" - case .minorAddNine: return "mAdd9" + case .minorAddNine: return "m(add9)" case .sixOverNine: return "6/9" + case .majorNinthFlatFive: return "maj9(♭5)" + case .majorNinthSharpFive: return "maj9(♯5)" case .majorEleventh: return "maj11" case .dominantEleventh: return "11" case .minorEleventh: return "m11" case .halfDiminishedEleventh: return "ø11" - case .majorSeventhFlatFive: return "maj7♭5" - case .majorSeventhSharpFive: return "maj7♯5" - case .majorNinthSharpEleventh: return "maj9♯11" - case .dominantFlatFive: return "7♭5" - case .dominantSharpFive: return "7♯5" + case .majorNinthSharpEleventh: return "maj9(♯11)" case .dominantFlatNinthSharpEleventh: return "7♭9♯11" case .dominantSharpNinthSharpEleventh: return "7♯9♯11" case .minorSeventhFlatNinthAddEleventh: return "m7♭9(add11)" @@ -284,8 +291,6 @@ extension ChordType: CustomStringConvertible { case .dominantThirteenth: return "13" case .minorEleventhFlatThirteenth: return "m11♭13" case .halfDiminishedFlatThirteenth: return "ø♭13" - case .majorNinthFlatFive: return "maj9♭5" - case .majorNinthSharpFive: return "maj9♯5" case .dominantNinthFlatFive: return "9♭5" case .dominantNinthSharpFive: return "9♯5" } @@ -315,11 +320,13 @@ extension ChordType: CustomStringConvertible { case .majorSeventh: return "^7" case .minorSeventh: return "m7" case .minorMajorSeventh: return "m^7" + case .majorSeventhFlatFive: return "^7b5" + case .augmentedMajorSeventh: return "^7#5" case .halfDiminishedNinth: return "Ø9" case .dominantNinth: return "9" case .dominantNinthSuspendedFourth: return "9sus4" - case .flatNinth: return "7b9" - case .sharpNinth: return "7#9" + case .dominantFlatNinth: return "7b9" + case .dominantSharpNinth: return "7#9" case .majorNinth: return "^9" case .minorMajorNinth: return "m^9" case .minorFlatNinth: return "m7b9" @@ -331,11 +338,9 @@ extension ChordType: CustomStringConvertible { case .dominantEleventh: return "11" case .minorEleventh: return "m11" case .halfDiminishedEleventh: return "Ø11" - case .majorSeventhFlatFive: return "^7b5" - case .majorSeventhSharpFive: return "^7#5" case .majorNinthSharpEleventh: return "^9#11" - case .dominantFlatFive: return "7b5" - case .dominantSharpFive: return "7#5" + case .dominantSeventhFlatFive: return "7b5" + case .dominantSeventhSharpFive: return "7#5" case .dominantFlatNinthSharpEleventh: return "7âÅ" case .dominantSharpNinthSharpEleventh: return "7åÅ" case .minorSeventhFlatNinthAddEleventh: return "m7b9(@11)" diff --git a/Sources/Tonic/Note.swift b/Sources/Tonic/Note.swift index f0058c5..c4df14c 100644 --- a/Sources/Tonic/Note.swift +++ b/Sources/Tonic/Note.swift @@ -85,6 +85,12 @@ public struct Note: Equatable, Hashable, Codable { public var noteNumber: Int8 { let octaveBounds = ((octave + 1) * 12) ... ((octave + 2) * 12) var note = Int(noteClass.letter.baseNote) + Int(noteClass.accidental.rawValue) + if noteClass.letter == .B && noteClass.accidental.rawValue > 0 { + note -= 12 + } + if noteClass.letter == .C && noteClass.accidental.rawValue < 0 { + note += 12 + } while !octaveBounds.contains(note) { note += 12 } @@ -136,7 +142,7 @@ public struct Note: Equatable, Hashable, Codable { let newLetterIndex = (noteClass.letter.rawValue + (shift.degree - 1)) let newLetter = Letter(rawValue: newLetterIndex % Letter.allCases.count)! let newMidiNoteNumber = Int(pitch.midiNoteNumber) + shift.semitones - + let newOctave = newMidiNoteNumber / 12 - 1 for accidental in Accidental.allCases { @@ -157,15 +163,39 @@ extension Note: Comparable { extension Note: IntRepresentable { public init(intValue: Int) { - octave = (intValue / 35) - 1 - let letter = Letter(rawValue: (intValue % 35) / 5)! - let accidental = Accidental(rawValue: Int8(intValue % 5) - 2)! + let accidentalCount = Accidental.allCases.count + let letterCount = Letter.allCases.count + let octaveCount = letterCount * accidentalCount + octave = (intValue / octaveCount) - 1 + var letter = Letter(rawValue: (intValue % octaveCount) / accidentalCount)! + var accidental = Accidental(rawValue: Int8(intValue % accidentalCount) - 2)! + + let index = intValue % octaveCount + if index == 0 { letter = .B; accidental = .sharp} + if index == 1 { letter = .B; accidental = .doubleSharp} + if index == octaveCount - 2 { letter = .C; accidental = .doubleFlat} + if index == octaveCount - 1 { letter = .C; accidental = .flat} + noteClass = NoteClass(letter, accidental: accidental) } - + /// Global index of the note for use in a NoteSet public var intValue: Int { - (octave + 1) * 7 * 5 + noteClass.letter.rawValue * 5 + (Int(noteClass.accidental.rawValue) + 2) + let accidentalCount = Accidental.allCases.count + let letterCount = Letter.allCases.count + let octaveCount = letterCount * accidentalCount + + var index = noteClass.letter.rawValue * accidentalCount + (Int(noteClass.accidental.rawValue) + 2) + if letter == .B { + if accidental == .sharp { index = 0} + if accidental == .doubleSharp { index = 1} + } + if letter == .C { + if accidental == .doubleFlat { index = octaveCount - 2} + if accidental == .flat { index = octaveCount - 1} + } + + return (octave + 1) * octaveCount + index } } diff --git a/Tests/TonicTests/ChordTests.swift b/Tests/TonicTests/ChordTests.swift index 1f06d47..f487ad9 100644 --- a/Tests/TonicTests/ChordTests.swift +++ b/Tests/TonicTests/ChordTests.swift @@ -2,6 +2,7 @@ import Tonic import XCTest class ChordTests: XCTestCase { + func testChords() { XCTAssertTrue(Chord.C.isTriad) XCTAssertEqual(Chord.Cs.description, "C♯") @@ -29,50 +30,49 @@ class ChordTests: XCTestCase { let notes: [Int8] = [60, 64, 66] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let chord = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(chord.map { $0.description }, ["C♭5"]) + XCTAssertEqual(chord.map { $0.slashDescription }, ["C♭5"]) } func testDominantSeventhFlatFive() { let notes: [Int8] = [60, 64, 66, 70] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let chord = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(chord.map { $0.description }, ["C7♭5", "F♯7♭5"]) + XCTAssertEqual(chord.map { $0.slashDescription }, ["C7♭5", "F♯7♭5/C"]) } func testMajorSeventhFlatFive() { let notes: [Int8] = [60, 64, 66, 71] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let chord = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(chord.map { $0.description }, ["Cmaj7♭5"]) + XCTAssertEqual(chord.map { $0.slashDescription }, ["Cmaj7♭5"]) } func testMajorNinthFlatFive() { let notes: [Int8] = [60, 64, 66, 71, 74] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let chord = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(chord.map { $0.description }, ["Cmaj9♭5"]) + XCTAssertEqual(chord.map { $0.slashDescription }, ["Cmaj9♭5"]) } func testMajorNinthSharpFive() { let notes: [Int8] = [60, 64, 68, 71, 74] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let chord = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(chord.map { $0.description }, ["Cmaj9♯5"]) + XCTAssertEqual(chord.map { $0.slashDescription }, ["Cmaj9♯5"]) } func testDominantNinthFlatFive() { let notes: [Int8] = [60, 64, 66, 70, 74] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let chord = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(chord.map { $0.description }, ["C9♭5", "D9♯5"]) + XCTAssertEqual(chord.map { $0.slashDescription }, ["C9♭5", "D9♯5/C"]) } - //TODO: - Test does not pass (returns "B♭9♭5"), requires update to getRankedChords algo to accomdate func testDominantNinthSharpFive() { let notes: [Int8] = [60, 64, 68, 70, 74] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let chord = Chord.getRankedChords(from: pitchSet) -// XCTAssertEqual(chord.map { $0.description }, ["C9♯5"]) + XCTAssertEqual(chord.map { $0.slashDescription }, ["C9♯5", "B♭9♭5/C"]) } func test7() { @@ -80,55 +80,59 @@ class ChordTests: XCTestCase { let notes: [Int8] = [60, 67, 70, 76] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let c7 = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(c7.map { $0.description }, ["C7"]) + XCTAssertEqual(c7.map { $0.slashDescription }, ["C7"]) + } + + func testTheortical() { + XCTAssertEqual(Chord(.C, type: .dominantSeventh).description, "C7") + let notes: [Int8] = [60, 67, 70, 76] + let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) + let c7 = Chord.getRankedChords(from: pitchSet, allowTheoreticalChords: true) + XCTAssertEqual(c7.map { $0.slashDescription }, ["C7", "D𝄫7", "B♯7"]) } func test7sus2() { let notes: [Int8] = [60, 62, 67, 70] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let c7sus2 = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(c7sus2.map { $0.description }, ["C7sus2"]) + XCTAssertEqual(c7sus2.map { $0.slashDescription }, ["C7sus2"]) } func test7sus4() { let notes: [Int8] = [60, 65, 67, 70] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let c7sus4 = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(c7sus4.map { $0.description }, ["C7sus4", "B♭6sus2", "F9sus4"]) + XCTAssertEqual(c7sus4.map { $0.slashDescription }, ["C7sus4", "B♭6sus2/C", "F9sus4/C"]) } func test9sus4() { let notes: [Int8] = [60, 65, 67, 74] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let c9sus4 = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(c9sus4.map { $0.description }, ["C9sus4", "G7sus4", "F6sus2"]) + XCTAssertEqual(c9sus4.map { $0.slashDescription }, ["C9sus4", "G7sus4/C", "F6sus2/C"]) } func test6sus2() { let notes: [Int8] = [60, 62, 67, 69] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let chord = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(chord.map { $0.description }, ["C6sus2", "G9sus4", "D7sus4"]) + XCTAssertEqual(chord.map { $0.slashDescription }, ["C6sus2", "G9sus4/C", "D7sus4/C"]) } func test6sus4() { let notes: [Int8] = [60, 65, 67, 69] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let chord = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(chord.map { $0.description }, ["C6sus4", "Fadd9"]) + XCTAssertEqual(chord.map { $0.slashDescription }, ["C6sus4", "Fadd9/C"]) } - /* - 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 - C C# D D# E F F# G G# A Bb B C C# D D# E F F# G G# A - */ func testMinorMajor7th() { let notes: [Int8] = [60, 63, 67, 71] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let chord = Chord.getRankedChords(from: pitchSet) let chord2 = Chord(.C, type: .minorMajorSeventh) - XCTAssertEqual(chord2.description, "CmMaj7") - XCTAssertEqual(chord.map { $0.description }, ["CmMaj7"]) + XCTAssertEqual(chord2.slashDescription, "CmMaj7") + XCTAssertEqual(chord.map { $0.slashDescription }, ["CmMaj7"]) } func testMinorMajor9th() { @@ -136,8 +140,8 @@ class ChordTests: XCTestCase { let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let chord = Chord.getRankedChords(from: pitchSet) let chord2 = Chord(.C, type: .minorMajorNinth) - XCTAssertEqual(chord2.description, "CmMaj9") - XCTAssertEqual(chord.map { $0.description }, ["CmMaj9"]) + XCTAssertEqual(chord2.slashDescription, "CmMaj9") + XCTAssertEqual(chord.map { $0.slashDescription }, ["CmMaj9"]) } func testMajor7thFlatFive() { @@ -145,8 +149,8 @@ class ChordTests: XCTestCase { let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) let chord = Chord.getRankedChords(from: pitchSet) let chord2 = Chord(.C, type: .majorSeventhFlatFive) - XCTAssertEqual(chord2.description, "Cmaj7♭5") - XCTAssertEqual(chord.map { $0.description }, ["Cmaj7♭5"]) + XCTAssertEqual(chord2.slashDescription, "Cmaj7♭5") + XCTAssertEqual(chord.map { $0.slashDescription }, ["Cmaj7♭5"]) } func testAugmentedDiminishededChordsPreferNoInversions() { @@ -286,14 +290,14 @@ class ChordTests: XCTestCase { let midiNotes: [Int8] = [54, 58, 61] let fSharp = PitchSet(pitches: midiNotes.map { Pitch($0) } ) let chords = Chord.getRankedChords(from: fSharp) - XCTAssertEqual(chords.map { $0.description }, ["G♭","F♯"]) + XCTAssertEqual(chords.map { $0.slashDescription }, ["G♭","F♯"]) } func testDuplicateRankedChords() { let midiNotes: [Int8] = [60, 64, 67] let pitchSet = PitchSet(pitches: midiNotes.map { Pitch($0) } ) - let cChords = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(cChords.map { $0.description }, ["C"]) + let cChords = Chord.getRankedChords(from: pitchSet, allowTheoreticalChords: true) + XCTAssertEqual(cChords.map { $0.slashDescription }, ["C", "D𝄫", "B♯"]) } func testPitchesWithNoInversion() { @@ -411,31 +415,25 @@ class ChordTests: XCTestCase { func assertChords(_ notes: [Int8], _ expected: [Chord]) { let pitchSet = PitchSet(pitches: notes.map { Pitch($0) }) let chords = Chord.getRankedChords(from: pitchSet) - XCTAssertEqual(chords.map { $0.slashDescription }, expected.map { $0.slashDescription }) + let isSubset = expected.allSatisfy {chords.contains($0) } + XCTAssertTrue(isSubset) } func testDiatonicChords() { - // Basic triads - assertChords([2, 6, 9], [.D]) - // We prioritize by the number of accidentals assertChords([1, 5, 8], [.Db, .Cs]) - + // Basic triads + assertChords([2, 6, 9], [.D]) // 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(.Db, type: .dominantSeventh), Chord(.Cs, type: .dominantSeventh),]) - assertChords([1, 5, 8, 11, 14], [Chord(.Cs, type: .flatNinth)]) + assertChords([1, 5, 8, 11, 14], [Chord(.Cs, type: .dominantFlatNinth)]) } func testClosedVoicing() { @@ -461,6 +459,11 @@ class ChordTests: XCTestCase { let resultSet = PitchSet(pitches: results.map { Pitch($0) }) XCTAssertEqual(pitchSet.transposedBassNoteTo(octave: -1), resultSet) } - - + + func testNewChords() { + let notes: [Int8] = [0, 3, 5, 7, 10] + let pitchSet = PitchSet(pitches: notes.map { Pitch($0) }) + let chords = Chord.getRankedChords(from: pitchSet) + print(chords.map {$0.slashDescription}) + } } diff --git a/Tests/TonicTests/KeyTests.swift b/Tests/TonicTests/KeyTests.swift index fe9162d..6a8d1aa 100644 --- a/Tests/TonicTests/KeyTests.swift +++ b/Tests/TonicTests/KeyTests.swift @@ -3,17 +3,17 @@ import XCTest class KeyTests: XCTestCase { func testKeyNotes() { - XCTAssertEqual(Key.C.noteSet.array.map { $0.noteClass.description }, - ["C", "D", "E", "F", "G", "A", "B"]) + XCTAssertEqual(Key.C.noteSet.array.map({ $0.noteClass.description }).sorted(), + ["A", "B", "C", "D", "E", "F", "G"]) - XCTAssertEqual(Key.Cm.noteSet.array.map { $0.noteClass.description }, - ["C", "D", "E♭", "F", "G", "A♭", "B♭"]) + XCTAssertEqual(Key.Cm.noteSet.array.sorted().map({ $0.noteClass.description }).sorted(), + ["A♭", "B♭", "C", "D", "E♭", "F", "G"]) - XCTAssertEqual(Key.Cs.noteSet.array.map { $0.noteClass.description }, - ["C♯", "D♯", "E♯", "F♯", "G♯", "A♯", "B♯"]) + XCTAssertEqual(Key.Cs.noteSet.array.sorted().map({ $0.noteClass.description }).sorted(), + ["A♯", "B♯", "C♯", "D♯", "E♯", "F♯", "G♯"]) - XCTAssertEqual(Key.Cb.noteSet.array.map { $0.noteClass.description }, - ["C♭", "D♭", "E♭", "F♭", "G♭", "A♭", "B♭"]) + XCTAssertEqual(Key.Cb.noteSet.array.sorted().map({ $0.noteClass.description }).sorted(), + ["A♭", "B♭","C♭", "D♭", "E♭", "F♭", "G♭"]) } func testKeyPrimaryTriads() {