From 60abb82705dbe111a77e5a9ccd32dfe80c2f12c7 Mon Sep 17 00:00:00 2001 From: Aurelius Prochazka Date: Sat, 10 Aug 2024 20:58:52 -0700 Subject: [PATCH] Switched to using Yamaha's standard as C3 for Middle C, which is Logic Pro's default --- Sources/Tonic/Note+MiddleCStandard.swift | 77 ++++++++++++++++++++++++ Sources/Tonic/Note.swift | 33 +++++----- Sources/Tonic/NoteClass.swift | 2 +- Sources/Tonic/Octave.swift | 5 +- Tests/TonicTests/ChordTests.swift | 2 +- Tests/TonicTests/NoteTests.swift | 52 ++++++++-------- Tests/TonicTests/TonicTests.swift | 6 +- 7 files changed, 129 insertions(+), 48 deletions(-) create mode 100644 Sources/Tonic/Note+MiddleCStandard.swift diff --git a/Sources/Tonic/Note+MiddleCStandard.swift b/Sources/Tonic/Note+MiddleCStandard.swift new file mode 100644 index 0000000..3fc22c7 --- /dev/null +++ b/Sources/Tonic/Note+MiddleCStandard.swift @@ -0,0 +1,77 @@ +// Borrowed from MIDIKit's MIDINote Style +// MIDIKit • https://github.com/orchetect/MIDIKit + +import Foundation + +extension Note { + /// MIDI note naming style (octave offset). + public enum MiddleCStandard: Equatable, Hashable, CaseIterable, Codable { + /// Yamaha (Middle C == C3) + /// + /// Yamaha traditionally chose "C3" to represent MIDI note 60 (Middle C). + case yamaha + + /// Roland (Middle C == C4) + /// + /// In 1982 Roland documentation writers chose "C4" to represent MIDI note 60 (Middle C). + case roland + + /// Cakewalk (Middle C == C5) + /// + /// Cakewalk originally chose "C5" to represent MIDI note 60 (Middle C). + /// + /// Cakewalk started life as a character-based DOS sequencer, and if they’d used "C4" or + /// "C3" for note 60, they’d have needed additional characters on-screen for notating the + /// lower octaves, e.g. "C-2". "C5" in effect sets the lowest octave to octave zero (C0). + case cakewalk + } +} + +extension Note.MiddleCStandard { + /// Returns the offset from zero for the first octave. + public var firstOctaveOffset: Int { + switch self { + case .yamaha: + return -2 + + case .roland: + return -1 + + case .cakewalk: + return 0 + } + } + + /// Returns the offset from zero for the first octave. + public var middleCNumber: Int { + switch self { + case .yamaha: + return 3 + + case .roland: + return 4 + + case .cakewalk: + return 5 + } + } +} + +extension Note.MiddleCStandard: CustomStringConvertible { + public var localizedDescription: String { + description + } + + public var description: String { + switch self { + case .yamaha: + return "Yamaha (Middle C == C3)" + + case .roland: + return "Roland (Middle C == C4)" + + case .cakewalk: + return "Cakewalk (Middle C == C5)" + } + } +} diff --git a/Sources/Tonic/Note.swift b/Sources/Tonic/Note.swift index c4df14c..4e859fb 100644 --- a/Sources/Tonic/Note.swift +++ b/Sources/Tonic/Note.swift @@ -4,6 +4,7 @@ import Foundation /// A pitch with a particular spelling. public struct Note: Equatable, Hashable, Codable { + /// Base name for the note public var noteClass: NoteClass = .init(.C, accidental: .natural) @@ -13,8 +14,11 @@ public struct Note: Equatable, Hashable, Codable { /// Convenience accessor for the accidental of the note public var accidental: Accidental { noteClass.accidental } - /// Range from -1 to 7 - public var octave: Int = 4 + /// Range from -2 to 8, with a dependency on the note style + public var octave: Int = Note.MiddleCStandard.yamaha.middleCNumber + + /// yamaha is the default for Logic Pro + public var middleCStandard: Note.MiddleCStandard = .yamaha /// Initialize the note /// @@ -24,7 +28,7 @@ public struct Note: Equatable, Hashable, Codable { /// - letter: Letter of the note /// - accidental: Accidental shift /// - octave: Which octave the note appears in - public init(_ letter: Letter = .C, accidental: Accidental = .natural, octave: Int = 4) { + public init(_ letter: Letter = .C, accidental: Accidental = .natural, octave: Int = Note.MiddleCStandard.yamaha.middleCNumber) { noteClass = NoteClass(letter, accidental: accidental) self.octave = octave } @@ -36,7 +40,7 @@ public struct Note: Equatable, Hashable, Codable { /// - pitch: Pitch, or essentially the midi note number of a note /// - key: Key in which to search for the appropriate note public init(pitch: Pitch, key: Key = .C) { - octave = Int(Double(pitch.midiNoteNumber) / 12) - 1 + octave = Int(Double(pitch.midiNoteNumber) / 12) + middleCStandard.firstOctaveOffset let pitchClass = pitch.pitchClass var noteInKey: Note? @@ -75,7 +79,7 @@ public struct Note: Equatable, Hashable, Codable { /// Initialize from raw value /// - Parameter index: integer represetnation public init(index: Int) { - octave = (index / 35) - 1 + octave = (index / 35) + middleCStandard.firstOctaveOffset let letter = Letter(rawValue: (index % 35) / 5)! let accidental = Accidental(rawValue: Int8(index % 5) - 2)! noteClass = NoteClass(letter, accidental: accidental) @@ -83,7 +87,7 @@ public struct Note: Equatable, Hashable, Codable { /// MIDI Note 0-127 starting at C public var noteNumber: Int8 { - let octaveBounds = ((octave + 1) * 12) ... ((octave + 2) * 12) + let octaveBounds = ((octave + middleCStandard.middleCNumber - 1) * 12) ... ((octave + middleCStandard.middleCNumber) * 12) var note = Int(noteClass.letter.baseNote) + Int(noteClass.accidental.rawValue) if noteClass.letter == .B && noteClass.accidental.rawValue > 0 { note -= 12 @@ -119,7 +123,7 @@ public struct Note: Equatable, Hashable, Codable { /// - Returns: New note the correct distance away public func shiftDown(_ shift: Interval) -> Note? { var newLetterIndex = (noteClass.letter.rawValue - (shift.degree - 1)) - let newOctave = (Int(pitch.midiNoteNumber) - shift.semitones) / 12 - 1 + let newOctave = (Int(pitch.midiNoteNumber) - shift.semitones) / 12 + middleCStandard.firstOctaveOffset while newLetterIndex < 0 { newLetterIndex += 7 @@ -142,9 +146,8 @@ 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 + let newOctave = (newMidiNoteNumber / 12) + middleCStandard.firstOctaveOffset for accidental in Accidental.allCases { let newNote = Note(newLetter, accidental: accidental, octave: newOctave) if newNote.noteNumber % 12 == newMidiNoteNumber % 12 { @@ -166,7 +169,7 @@ extension Note: IntRepresentable { let accidentalCount = Accidental.allCases.count let letterCount = Letter.allCases.count let octaveCount = letterCount * accidentalCount - octave = (intValue / octaveCount) - 1 + octave = (intValue / octaveCount) + middleCStandard.firstOctaveOffset var letter = Letter(rawValue: (intValue % octaveCount) / accidentalCount)! var accidental = Accidental(rawValue: Int8(intValue % accidentalCount) - 2)! @@ -187,15 +190,15 @@ extension Note: IntRepresentable { 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 accidental == .sharp { index = 0 } + if accidental == .doubleSharp { index = 1 } } if letter == .C { - if accidental == .doubleFlat { index = octaveCount - 2} - if accidental == .flat { index = octaveCount - 1} + if accidental == .doubleFlat { index = octaveCount - 2 } + if accidental == .flat { index = octaveCount - 1 } } - return (octave + 1) * octaveCount + index + return (octave + middleCStandard.middleCNumber - 1) * octaveCount + index } } diff --git a/Sources/Tonic/NoteClass.swift b/Sources/Tonic/NoteClass.swift index 133e29a..07e9da0 100644 --- a/Sources/Tonic/NoteClass.swift +++ b/Sources/Tonic/NoteClass.swift @@ -12,7 +12,7 @@ public struct NoteClass: Equatable, Hashable, Codable { /// Accidental of the note class public var accidental: Accidental - private static let canonicalOctave = 4 + private static let canonicalOctave = Note.MiddleCStandard.yamaha.middleCNumber /// A representative note for this class, in the canonical octave, which is 4 public var canonicalNote: Note { diff --git a/Sources/Tonic/Octave.swift b/Sources/Tonic/Octave.swift index 6dd66e5..e81ad8e 100644 --- a/Sources/Tonic/Octave.swift +++ b/Sources/Tonic/Octave.swift @@ -3,6 +3,7 @@ import Foundation /// Private Octave enumeration for octave related functions /// Will make it public once the entirety of Tonic uses it well enum Octave: Int { + case negative2 = -2 case negative1 = -1 case zero = 0 case one = 1 @@ -15,8 +16,8 @@ enum Octave: Int { case eight = 8 case nine = 9 - init?(of pitch:Pitch) { - let octaveInt = Int(pitch.midiNoteNumber) / 12 - 1 + init?(of pitch:Pitch, style: Note.MiddleCStandard = .roland ) { + let octaveInt = Int(pitch.midiNoteNumber) / 12 + style.firstOctaveOffset if let octave = Octave(rawValue: octaveInt) { self = octave } else { diff --git a/Tests/TonicTests/ChordTests.swift b/Tests/TonicTests/ChordTests.swift index 7bca3f6..e4f7e9e 100644 --- a/Tests/TonicTests/ChordTests.swift +++ b/Tests/TonicTests/ChordTests.swift @@ -276,7 +276,7 @@ class ChordTests: XCTestCase { XCTAssertEqual(gSus4.description, "Gsus4") // To deal with this, you have to tell Tonic that you want an array of potential chords - let gChords = Chord.getRankedChords(from: [.C, .D, Note(.G, octave: 3)]) + let gChords = Chord.getRankedChords(from: [.C, .D, Note(.G, octave: 2)]) // What we want is for this to list "Gsus4 first and Csus2 second whereas let cChords = Chord.getRankedChords(from: [.C, .D, .G]) diff --git a/Tests/TonicTests/NoteTests.swift b/Tests/TonicTests/NoteTests.swift index ed153de..ce91744 100644 --- a/Tests/TonicTests/NoteTests.swift +++ b/Tests/TonicTests/NoteTests.swift @@ -3,17 +3,17 @@ import XCTest final class NoteTests: XCTestCase { func testNoteOctave() { - let c4 = Note.C - XCTAssertEqual(c4.noteNumber, 60) - XCTAssertEqual(c4.description, "C4") + let middleC = Note.C + XCTAssertEqual(middleC.noteNumber, 60) + XCTAssertEqual(middleC.description, "C3") - let cb4 = Note(.C, accidental: .flat, octave: 4) - XCTAssertEqual(cb4.noteNumber, 71) - XCTAssertEqual(cb4.description, "C♭4") + let cb3 = Note(.C, accidental: .flat, octave: 3) + XCTAssertEqual(cb3.noteNumber, 71) + XCTAssertEqual(cb3.description, "C♭3") - let c5 = Note(.C, octave: 5) - XCTAssertEqual(c5.noteNumber, 72) - XCTAssertEqual(c5.description, "C5") + let c4 = Note(.C, octave: 4) + XCTAssertEqual(c4.noteNumber, 72) + XCTAssertEqual(c4.description, "C4") } // From: https://github.com/AudioKit/Tonic/issues/16 @@ -23,33 +23,33 @@ final class NoteTests: XCTestCase { // "Accidentals applied to a note do not have an effect on its ASPN number. For example, B♯3 and C4 have different octave numbers despite being enharmonically equivalent, because the B♯ is still considered part of the lower octave." func testThatOctaveRefersToAccidentalLessBaseNote() { let bs3 = Note(.B, accidental: .sharp, octave: 3) - XCTAssertEqual(bs3.noteNumber, 48) + XCTAssertEqual(bs3.noteNumber, 60) XCTAssertEqual(bs3.description, "B♯3") let cb4 = Note(.C, accidental: .flat, octave: 4) - XCTAssertEqual(cb4.noteNumber, 71) + XCTAssertEqual(cb4.noteNumber, 83) XCTAssertEqual(cb4.description, "C♭4") } func testNoteSpelling() { let dFlat = Note.Db XCTAssertEqual(dFlat.noteNumber, 61) - XCTAssertEqual(dFlat.description, "D♭4") + XCTAssertEqual(dFlat.description, "D♭3") XCTAssertEqual(dFlat.spelling(in: Key.C).description, "C♯") XCTAssertEqual(dFlat.spelling(in: Key.F).description, "D♭") let cSharp = Note.Cs XCTAssertEqual(cSharp.noteNumber, 61) - XCTAssertEqual(cSharp.description, "C♯4") + XCTAssertEqual(cSharp.description, "C♯3") XCTAssertEqual(cSharp.spelling(in: Key.Ab).description, "D♭") let dDoubleFlat = Note(.D, accidental: .doubleFlat) XCTAssertEqual(dDoubleFlat.noteNumber, 60) - XCTAssertEqual(dDoubleFlat.description, "D𝄫4") + XCTAssertEqual(dDoubleFlat.description, "D𝄫3") let cDoubleSharp = Note(accidental: .doubleSharp) XCTAssertEqual(cDoubleSharp.noteNumber, 62) - XCTAssertEqual(cDoubleSharp.description, "C𝄪4") + XCTAssertEqual(cDoubleSharp.description, "C𝄪3") } func testComparison() { @@ -58,34 +58,34 @@ final class NoteTests: XCTestCase { func testNoteShift() { let d = Note(.C).shiftUp(.M2) - XCTAssertEqual(d!.description, "D4") + XCTAssertEqual(d!.description, "D3") let eFlat = Note(.C).shiftUp(.m3) - XCTAssertEqual(eFlat!.description, "E♭4") + XCTAssertEqual(eFlat!.description, "E♭3") let db = Note(.C).shiftDown(.M7) - XCTAssertEqual(db!.description, "D♭3") + XCTAssertEqual(db!.description, "D♭2") let ebbb = Note(.F, accidental: .doubleFlat).shiftDown(.M2) XCTAssertNil(ebbb) let c = Note(.D).shiftDown(.M2) - XCTAssertEqual(c!.description, "C4") + XCTAssertEqual(c!.description, "C3") let cs = Note(.D).shiftDown(.m2) - XCTAssertEqual(cs!.description, "C♯4") + XCTAssertEqual(cs!.description, "C♯3") let eb = Note(.B).shiftUp(.d4) - XCTAssertEqual(eb!.description, "E♭5") + XCTAssertEqual(eb!.description, "E♭4") let fs = Note(.C).shiftUp(.A4) - XCTAssertEqual(fs!.description, "F♯4") + XCTAssertEqual(fs!.description, "F♯3") let asharp = Note(.C).shiftUp(.A6) - XCTAssertEqual(asharp!.description, "A♯4") + XCTAssertEqual(asharp!.description, "A♯3") let c6 = Note(.G).shiftUp(.P11) - XCTAssertEqual(c6!.description, "C6") + XCTAssertEqual(c6!.description, "C5") let g = Note(.C, octave: 6).shiftDown(.P11) XCTAssertEqual(g!.description, "G4") @@ -131,11 +131,11 @@ final class NoteTests: XCTestCase { func testNoteDistance() { XCTAssertEqual(Note.C.semitones(to: Note.D), 2) XCTAssertEqual(Note.C.semitones(to: Note.G), 7) - XCTAssertEqual(Note.C.semitones(to: Note(.G, octave: 3)), 5) + XCTAssertEqual(Note.C.semitones(to: Note(.G, octave: 2)), 5) } func testNoteIntValue() { - let lowest = Note(.C, octave: -1).intValue + let lowest = Note(.C, octave: -2).intValue let highest = Note(pitch: Pitch(127), key: .C).intValue for i in lowest ..< highest { diff --git a/Tests/TonicTests/TonicTests.swift b/Tests/TonicTests/TonicTests.swift index a6acce5..fd9e65e 100644 --- a/Tests/TonicTests/TonicTests.swift +++ b/Tests/TonicTests/TonicTests.swift @@ -12,9 +12,9 @@ final class TonicTests: XCTestCase { } func testNoteIndex() { - let c4 = Note.C - let index = c4.intValue - XCTAssertEqual(c4, Note(index: index)) + let c3 = Note.C + let index = c3.intValue + XCTAssertEqual(c3, Note(index: index)) } func testPitch() {