Skip to content

Commit

Permalink
Switched to using Yamaha's standard as C3 for Middle C, which is Logi…
Browse files Browse the repository at this point in the history
…c Pro's default
  • Loading branch information
aure committed Aug 11, 2024
1 parent 2461142 commit 60abb82
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 48 deletions.
77 changes: 77 additions & 0 deletions Sources/Tonic/Note+MiddleCStandard.swift
Original file line number Diff line number Diff line change
@@ -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)"
}
}
}
33 changes: 18 additions & 15 deletions Sources/Tonic/Note.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
///
Expand All @@ -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
}
Expand All @@ -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?
Expand Down Expand Up @@ -75,15 +79,15 @@ 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)
}

/// 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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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)!

Expand All @@ -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
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Tonic/NoteClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions Sources/Tonic/Octave.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion Tests/TonicTests/ChordTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
52 changes: 26 additions & 26 deletions Tests/TonicTests/NoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand All @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions Tests/TonicTests/TonicTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit 60abb82

Please sign in to comment.