From 4c7a8f9b9b9d025975fc8bc97081f12b3597cdd6 Mon Sep 17 00:00:00 2001 From: Andrew Struck-Marcell Date: Thu, 22 Oct 2026 14:01:53 +0900 Subject: [PATCH] Add a few more chords --- Sources/Tonic/ChordType.swift | 37 +++++++++++++++++++++++--- Tests/TonicTests/ChordTests.swift | 42 +++++++++++++++++++++++++++--- Tests/TonicTests/KeyTests.swift | 2 +- Tests/TonicTests/ReadMeTests.swift | 4 +-- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/Sources/Tonic/ChordType.swift b/Sources/Tonic/ChordType.swift index 31b091c..209756b 100644 --- a/Sources/Tonic/ChordType.swift +++ b/Sources/Tonic/ChordType.swift @@ -4,9 +4,13 @@ import Foundation /// Chord type as defined by a set of intervals from a root note class /// We use abreviated descriptions with extensions seperated by underscores -/// We don't like usiing underscores for enum cases any more than you do, however is a nice visual speration of extensions +/// We don't like using underscores for enum cases any more than you do, however is a nice visual speration of extensions public enum ChordType: String, Sendable, CaseIterable, Codable { + //MARK: - Dyads + /// Five: Perfect Fifth, e.g. `C5` + case five + //MARK: - Triads /// Major Triad: Major Third, Perfect Fifth, e.g. `C` case major @@ -99,6 +103,9 @@ public enum ChordType: String, Sendable, CaseIterable, Codable { /// Major Add Four: Major Third, Perfect Fifth, Perfect Eleventh, e.g. `C(add4)` case maj_add4 + /// Major Add Sharp Four: Major Third, Perfect Fifth, Sharp Eleventh, e.g. `C(add♯4)` + case maj_addSharp4 + /// Minor Add Nine: Minor Third, Perfect Fifth, Major Ninth, e.g. `Cm(add9)` case min_add9 @@ -134,7 +141,10 @@ public enum ChordType: String, Sendable, CaseIterable, Codable { /// Six Over Nine: Major Third, Perfect Fifth, Major Sixth, Major Ninth, e.g. `C6/9` case maj_6_9 - + + /// Minor Six Over Nine: Minor Third, Perfect Fifth, Major Sixth, Major Ninth, e.g. `Cm6/9` + case min_6_9 + /// Augmented Major Ninth: Major Third, Augmented Fifth, Major Seventh, Major Nine, e.g. `Cmaj9(♯5)` case maj9_sharp5 @@ -219,7 +229,10 @@ public enum ChordType: String, Sendable, CaseIterable, Codable { /// Major Seventh Add Sharp Eleventh: Major Third, Perfect Fifth, Major Seventh, Augmented Eleventh, e.g. `Cmaj7(add♯11)` case maj7_addSharp11 - + + /// Major Seventh Sharp Five Add Sharp Eleventh: Major Third, Augmented Fifth, Major Seventh, Augmented Eleventh, e.g. `Cmaj7(♯5)(add♯11)` + case maj7_sharp5_addSharp11 + /// Dominant Seventh Add Eleventh: Major Third, Perfect Fifth, Minor Seventh, Perfect Eleventh, e.g. `C7(add11)` case dom7_add11 @@ -554,7 +567,9 @@ public enum ChordType: String, Sendable, CaseIterable, Codable { public var intervals: [Interval] { switch self { - case .major: + case .five: + return [.P5] + case .major: return [.M3, .P5] case .minor: return [.m3, .P5] @@ -634,6 +649,8 @@ public enum ChordType: String, Sendable, CaseIterable, Codable { return [.M3, .P5, .M9] case .maj_add4: return [.M3, .P5, .P11] + case .maj_addSharp4: + return [.M3, .P5, .A11] case .min_add9: return [.m3, .P5, .M9] case .dim_add9: @@ -658,6 +675,8 @@ public enum ChordType: String, Sendable, CaseIterable, Codable { return [.M3, .A5, .A9] case .maj_6_9: return [.M3, .P5, .M6, .M9] + case .min_6_9: + return [.m3, .P5, .M6, .M9] case .maj9_sharp5: return [.M3, .A5, .M7, .M9] case .maj9_flat5: @@ -712,6 +731,8 @@ public enum ChordType: String, Sendable, CaseIterable, Codable { return [.M3, .P5, .M7, .P11] case .maj7_addSharp11: return [.M3, .P5, .M7, .A11] + case .maj7_sharp5_addSharp11: + return [.M3, .A5, .M7, .A11] case .dom7_add11: return [.M3, .P5, .m7, .P11] case .dom7_addSharp11: @@ -918,6 +939,7 @@ extension ChordType: CustomStringConvertible { /// Adornment to the Root NoteClass (letter+accidental) that defines the chord type public var description: String { switch self { + case .five: return "5" case .major: return "" case .minor: return "m" case .dim: return "°" @@ -952,8 +974,10 @@ extension ChordType: CustomStringConvertible { case .min_maj9: return "mMaj9" case .maj_add9: return "(add9)" case .maj_add4: return "(add4)" + case .maj_addSharp4: return "(add♯4)" case .min_add9: return "m(add9)" case .maj_6_9: return "6/9" + case .min_6_9: return "m(add6)(add9)" case .maj9_flat5: return "maj9(♭5)" case .maj11: return "maj11" case .dom11: return "11" @@ -997,6 +1021,7 @@ extension ChordType: CustomStringConvertible { case .min7_flat9_sharp11: return "m7(♭9)(♯11)" case .maj7_add11: return "maj7(add11)" case .maj7_addSharp11: return "maj7(add♯11)" + case .maj7_sharp5_addSharp11: return "maj7(♯5)(add♯11)" case .dom7_add11: return "7(add11)" case .dom7_addSharp11: return "7(add♯11)" case .min7_add11: return "m7(add11)" @@ -1104,6 +1129,7 @@ extension ChordType: CustomStringConvertible { /// NotationExpress: https://www.notationcentral.com/product/norfolk-fonts-for-sibelius/ public var chordFontDescription: String { switch self { + case .five: return "5" case .major: return "" case .minor: return "m" case .dim: return "º" @@ -1138,8 +1164,10 @@ extension ChordType: CustomStringConvertible { case .min_maj9: return "m^9" case .maj_add9: return "@9" case .maj_add4: return "@4" + case .maj_addSharp4: return "#4" case .min_add9: return "m@9" case .maj_6_9: return "%" + case .min_6_9: return "m@6@9" case .maj9_flat5: return "^9b5" case .maj11: return "^11" case .dom11: return "11" @@ -1183,6 +1211,7 @@ extension ChordType: CustomStringConvertible { case .min7_flat9_sharp11: return "m7b9#11" case .maj7_add11: return "^7@11" case .maj7_addSharp11: return "^7@#11" + case .maj7_sharp5_addSharp11: return "^7#5@#11" case .dom7_add11: return "7@11" case .dom7_addSharp11: return "7@#11" case .min7_add11: return "m7@11" diff --git a/Tests/TonicTests/ChordTests.swift b/Tests/TonicTests/ChordTests.swift index 3372e05..0f8bb91 100644 --- a/Tests/TonicTests/ChordTests.swift +++ b/Tests/TonicTests/ChordTests.swift @@ -168,6 +168,15 @@ class ChordTests: XCTestCase { XCTAssertEqual(chord.map { $0.slashDescription }, ["CmMaj9"]) } + func testMinorMajor7thFlat9th() { + let notes: [Int8] = [60, 63, 67, 71, 73] + let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) + let chord = Chord.getRankedChords(from: pitchSet) + let chord2 = Chord(.C, type: .min_maj_flat9) + XCTAssertEqual(chord2.slashDescription, "CmMaj(♭9)") + XCTAssertEqual(chord.map { $0.slashDescription }, ["CmMaj(♭9)"]) + } + func testMajor7thFlatFive() { let notes: [Int8] = [60, 64, 66, 71] let pitchSet = PitchSet(pitches: notes.map { Pitch($0) } ) @@ -242,6 +251,9 @@ class ChordTests: XCTestCase { let C69 = Chord(notes: [.C, .E, .G, .A, .D]) XCTAssertEqual(C69?.description, "C6/9") + + let Cm69 = Chord(notes: [.C, .Eb, .G, .A, .D]) + XCTAssertEqual(Cm69?.description, "Cm(add6)(add9)") } func testEleventhNaming() { @@ -506,8 +518,17 @@ class ChordTests: XCTestCase { print(chords.map {$0.slashDescription}) } + // MARK: - Dyads + + func testPowerChords() { + XCTAssertEqual(Chord(notes: [.C, .G])?.description, "C5") + assertRankedChord([60, 67], expectedDescriptions: ["C5"]) + assertRankedChord([67, 74], expectedDescriptions: ["G5"]) + assertRankedChord([55, 60], expectedDescriptions: ["C5/G"]) + } + // MARK: - Triads - + func testMajorTriads() { assertRankedChord([60, 64, 67], expectedDescriptions: ["C"]) assertRankedChord([65, 69, 72], expectedDescriptions: ["F"]) @@ -610,7 +631,17 @@ class ChordTests: XCTestCase { assertRankedChord([60, 64, 67, 74], expectedDescriptions: ["C(add9)", "Gsus4(add13)/C"]) assertRankedChord([65, 69, 72, 79], expectedDescriptions: ["F(add9)", "Csus4(add13)/F"]) } - + + func testAddSharp4Chords() { + assertRankedChord([60, 64, 66, 67], expectedDescriptions: ["C(add♯4)"]) + assertRankedChord([65, 69, 71, 72], expectedDescriptions: ["F(add♯4)"]) + } + + func testMinorSixNineChords() { + assertRankedChord([60, 63, 67, 69, 74], expectedDescriptions: ["Cm(add6)(add9)", "Aø7(add11)/C", "D7sus4(♭9)/C"]) + assertRankedChord([62, 65, 69, 71, 76], expectedDescriptions: ["Dm(add6)(add9)", "Bø7(add11)/D", "E7sus4(♭9)/D"]) + } + // MARK: - Altered Chords func testFlatFiveChords() { @@ -622,7 +653,12 @@ class ChordTests: XCTestCase { assertRankedChord([60, 64, 68], expectedDescriptions: ["C⁺", "A♭⁺/C"]) assertRankedChord([65, 69, 73], expectedDescriptions: ["F⁺", "D♭⁺/F"]) } - + + func testMajorSeventhSharpFiveAddSharpEleventhChords() { + assertRankedChord([60, 64, 68, 71, 78], expectedDescriptions: ["Cmaj7(♯5)(add♯11)"]) + assertRankedChord([65, 69, 73, 76, 83], expectedDescriptions: ["Fmaj7(♯5)(add♯11)"]) + } + // MARK: - Inversions func testFirstInversion() { diff --git a/Tests/TonicTests/KeyTests.swift b/Tests/TonicTests/KeyTests.swift index 791f650..2a3619a 100644 --- a/Tests/TonicTests/KeyTests.swift +++ b/Tests/TonicTests/KeyTests.swift @@ -41,7 +41,7 @@ class KeyTests: XCTestCase { } func testKeyChords() { - XCTAssertEqual(Key.G.chords.count, 60) // This should only change if new chord types are added + XCTAssertEqual(Key.G.chords.count, 67) // This should only change if new chord types are added for triad in Key.G.primaryTriads { XCTAssert(Key.G.chords.contains(triad)) } diff --git a/Tests/TonicTests/ReadMeTests.swift b/Tests/TonicTests/ReadMeTests.swift index 6c71f69..ee0e548 100644 --- a/Tests/TonicTests/ReadMeTests.swift +++ b/Tests/TonicTests/ReadMeTests.swift @@ -16,12 +16,12 @@ final class ReadMeTests: XCTestCase { // What chords are in this key? func testChordsInKey() { - XCTAssertEqual(Key.Cm.chords.count, 60) + XCTAssertEqual(Key.Cm.chords.count, 67) } // What chords in this key contain this note? func testChordsInKeyContainNote() { - XCTAssertEqual(Key.C.chords.filter { $0.noteClasses.contains(.C) }.count, 36) + XCTAssertEqual(Key.C.chords.filter { $0.noteClasses.contains(.C) }.count, 39) } // What notes do these keys have in common?