Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions Sources/Tonic/ChordType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Could also be "no3"

case .major: return ""
case .minor: return "m"
case .dim: return "°"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)"
Expand Down Expand Up @@ -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 "º"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
42 changes: 39 additions & 3 deletions Tests/TonicTests/ChordTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) } )
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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"])
Expand Down Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion Tests/TonicTests/KeyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/TonicTests/ReadMeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down