Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Sources/Tonic/Chord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public struct Chord: Equatable, Codable {
/// - Returns: Roman Numeral notation
public func romanNumeralNotation(in key: Key) -> String? {
let capitalRomanNumerals = ["I", "II", "III", "IV", "V", "VI", "VII"]
if let index = key.primaryTriads.firstIndex(where: { $0 == self }) {
if let index = key.primaryTriads().firstIndex(where: { $0 == self }) {
let romanNumeral = capitalRomanNumerals[index]
switch type {
case .major: return romanNumeral
Expand Down
57 changes: 31 additions & 26 deletions Sources/Tonic/Key.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Foundation
/// The key is the set of notes that are played in a composition, or portion of a composition.
///
/// A key is composed of a Root ``Note``, and a ``Scale``.
public struct Key: Equatable {
public struct Key: Equatable, Codable {
/// The primary note class of the key, also known as the tonic
public let root: NoteClass

Expand All @@ -15,12 +15,6 @@ public struct Key: Equatable {
/// A note set containing all the notes in the key
public let noteSet: NoteSet

/// All the traditional triads representable root, third, and fifth from each note in the key
public let primaryTriads: [Chord]

/// All chords that fit in the key
public let chords: [Chord]

/// Initialize the key
/// - Parameters:
/// - root: The primary note class of the key, also known as the tonic
Expand All @@ -36,7 +30,36 @@ public struct Key: Equatable {
}
}
noteSet = NoteSet(notes: r)
}

/// The type of accidental to use in this key
public var preferredAccidental: Accidental {
if root.accidental == .sharp {
return .sharp
}
if root.accidental == .flat {
return .flat
}

let naturalKeysWithFlats: [Key] = [.F, .d, .g, .c, .f]
if naturalKeysWithFlats.contains(self) {
return .flat
}
return .sharp
}

/// All chords that fit in the key
public func chords() -> [Chord] {
let table = ChordTable.shared
var chords: [Chord] = []
for (_, chord) in table.chords where chord.noteClassSet.isSubset(of: noteSet.noteClassSet) {
chords.append(Chord(chord.root, type: chord.type))
}
return chords
}

/// All the traditional triads representable root, third, and fifth from each note in the key
public func primaryTriads() -> [Chord] {
let table = ChordTable.shared

var chords: [Chord] = []
Expand All @@ -53,25 +76,7 @@ public struct Key: Equatable {

let primaryTriadsStartingWithC = primaryTriads.sorted(by: { $0.root.letter < $1.root.letter })
let rootPosition = primaryTriadsStartingWithC.firstIndex(where: { $0.root == root }) ?? 0
self.primaryTriads = Array(primaryTriadsStartingWithC.rotatingLeft(positions: rootPosition))

self.chords = chords
}

/// The type of accidental to use in this key
public var preferredAccidental: Accidental {
if root.accidental == .sharp {
return .sharp
}
if root.accidental == .flat {
return .flat
}

let naturalKeysWithFlats: [Key] = [.F, .d, .g, .c, .f]
if naturalKeysWithFlats.contains(self) {
return .flat
}
return .sharp
return Array(primaryTriadsStartingWithC.rotatingLeft(positions: rootPosition))
}
}

Expand Down
10 changes: 5 additions & 5 deletions Tests/TonicTests/ChordTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,15 @@ class ChordTests: XCTestCase {
}

func testRomanNumerals() {
XCTAssertEqual(Key.C.primaryTriads.map { $0.romanNumeralNotation(in: Key.C) ?? "" },
XCTAssertEqual(Key.C.primaryTriads().map { $0.romanNumeralNotation(in: Key.C) ?? "" },
["I", "ii", "iii", "IV", "V", "vi", "vii°"])
XCTAssertEqual(Key.C.primaryTriads.map { $0.romanNumeralNotation(in: Key.Am) ?? "" },
XCTAssertEqual(Key.C.primaryTriads().map { $0.romanNumeralNotation(in: Key.Am) ?? "" },
["III", "iv", "v", "VI", "VII", "i", "ii°"])
XCTAssertEqual(Key.C.primaryTriads.map { $0.romanNumeralNotation(in: Key.G) ?? "" },
XCTAssertEqual(Key.C.primaryTriads().map { $0.romanNumeralNotation(in: Key.G) ?? "" },
["IV", "", "vi", "", "I", "ii", ""])
XCTAssertEqual(Key.Am.primaryTriads.map { $0.romanNumeralNotation(in: Key.Am) ?? "" },
XCTAssertEqual(Key.Am.primaryTriads().map { $0.romanNumeralNotation(in: Key.Am) ?? "" },
["i", "ii°", "III", "iv", "v", "VI", "VII"])
XCTAssertEqual(Key.Am.primaryTriads.map { $0.romanNumeralNotation(in: Key.C) ?? "" },
XCTAssertEqual(Key.Am.primaryTriads().map { $0.romanNumeralNotation(in: Key.C) ?? "" },
["vi", "vii°", "I", "ii", "iii", "IV", "V"])
}

Expand Down
22 changes: 11 additions & 11 deletions Tests/TonicTests/KeyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,33 @@ class KeyTests: XCTestCase {
}

func testKeyPrimaryTriads() {
XCTAssertEqual(Key.C.primaryTriads.map { $0.description },
XCTAssertEqual(Key.C.primaryTriads().map { $0.description },
["C", "Dm", "Em", "F", "G", "Am", "B°"])
XCTAssertEqual(Key.Am.primaryTriads.map { $0.description },
XCTAssertEqual(Key.Am.primaryTriads().map { $0.description },
["Am", "B°", "C", "Dm", "Em", "F", "G"])
XCTAssertEqual(Key.G.primaryTriads.map { $0.description },
XCTAssertEqual(Key.G.primaryTriads().map { $0.description },
["G", "Am", "Bm", "C", "D", "Em", "F♯°"])
XCTAssertEqual(Key.Cs.primaryTriads.map { $0.description },
XCTAssertEqual(Key.Cs.primaryTriads().map { $0.description },
["C♯", "D♯m", "E♯m", "F♯", "G♯", "A♯m", "B♯°"])
XCTAssertEqual(Key.Cb.primaryTriads.map { $0.description },
XCTAssertEqual(Key.Cb.primaryTriads().map { $0.description },
["C♭", "D♭m", "E♭m", "F♭", "G♭", "A♭m", "B♭°"])
}

func testScalePrimaryTriads() {
XCTAssertEqual(Key(root: .C, scale: .harmonicMinor).primaryTriads.map { $0.description },
XCTAssertEqual(Key(root: .C, scale: .harmonicMinor).primaryTriads().map { $0.description },
["Cm", "D°", "E♭⁺", "Fm", "G", "A♭", "B°"])

XCTAssertEqual(Key(root: .Db, scale: .phrygian).primaryTriads.map { $0.description },
XCTAssertEqual(Key(root: .Db, scale: .phrygian).primaryTriads().map { $0.description },
["D♭m", "E𝄫", "F♭", "G♭m", "A♭°", "B𝄫", "C♭m"])

XCTAssertEqual(Key(root: .Ds, scale: .harmonicMinor).primaryTriads.map { $0.description },
XCTAssertEqual(Key(root: .Ds, scale: .harmonicMinor).primaryTriads().map { $0.description },
["D♯m", "E♯°", "F♯⁺", "G♯m", "A♯", "B", "C𝄪°"])
}

func testKeyChords() {
XCTAssertEqual(Key.G.chords.count, 60) // This should only change if new chord types are added
for triad in Key.G.primaryTriads {
XCTAssert(Key.G.chords.contains(triad))
XCTAssertEqual(Key.G.chords().count, 60) // 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, 60)
}

// 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, 36)
}

// What notes do these keys have in common?
Expand Down