diff --git a/Sources/MIDIKit/Note/MIDI Note Layout.swift b/Sources/MIDIKit/Note/MIDI Note Layout.swift index 4175cef0e0..2e55dc23ad 100644 --- a/Sources/MIDIKit/Note/MIDI Note Layout.swift +++ b/Sources/MIDIKit/Note/MIDI Note Layout.swift @@ -7,11 +7,11 @@ import Foundation extension MIDI { - public typealias NoteRange = ClosedRange + public typealias NoteNumberRange = ClosedRange } -extension MIDI.NoteRange { +extension MIDI.NoteNumberRange { /// All 128 notes (C-2...G8, or 0...127) @inline(__always) @@ -25,21 +25,35 @@ extension MIDI.NoteRange { extension MIDI { - public enum PianoKeyType { - - case white - case black - - } + public typealias NoteRange = ClosedRange + +} + +extension MIDI.NoteRange { + + /// All 128 notes (C-2...G8, or 0...127) + @inline(__always) + public static let all: Self = MIDI.Note(0)...MIDI.Note(127) + + /// 88-key piano keyboard note range: (A-1...C7, or 12...108) + @inline(__always) + public static let eightyEightKeys: Self = MIDI.Note(21)...MIDI.Note(108) } extension MIDI.Note { - public var pianoKey: MIDI.PianoKeyType { + /// Returns `true` if note is sharp (has a ♯ accidental). On a piano keyboard, this would be a black key. + @inline(__always) + public var isSharp: Bool { let octaveMod = number % 12 - return [0,2,4,5,7,9,11].contains(octaveMod) ? .white : .black + return [1,3,6,8,10].contains(octaveMod) + + + // this also works, but the math above may be slightly more performant, + // since the `name` property would have to call `Name.convert(noteNumber:)` + //return name.isSharp } diff --git a/Sources/MIDIKit/Note/MIDI Note Name.swift b/Sources/MIDIKit/Note/MIDI Note Name.swift index 65864a0258..b1eebad57e 100644 --- a/Sources/MIDIKit/Note/MIDI Note Name.swift +++ b/Sources/MIDIKit/Note/MIDI Note Name.swift @@ -200,7 +200,7 @@ extension MIDI.Note { } - /// Semitone offset, originating from note C + /// Semitone offset originating from note C, ascending. public var scaleOffset: Int { switch self { @@ -220,6 +220,56 @@ extension MIDI.Note { } + /// Returns `true` if note is sharp (has a ♯ accidental). On a piano keyboard, this would be a black key. + public var isSharp: Bool { + + switch self { + case .A, + .B, + .C, + .D, + .E, + .F, + .G: + return false + + case .A_sharp, + .C_sharp, + .D_sharp, + .F_sharp, + .G_sharp: + return true + } + + } + + /// Returns note name and octave for the MIDI note number. + /// Returns `nil` if MIDI note number is invalid. + internal static func convert(noteNumber: MIDI.UInt7) -> (name: Self, octave: Int) { + // UInt7 is guaranteed to be a valid MIDI note number + + let octave = (noteNumber.intValue / 12) - 2 + + switch noteNumber.intValue % 12 { + case 9: return (name: .A, octave: octave) + case 10: return (name: .A_sharp, octave: octave) + case 11: return (name: .B, octave: octave) + case 0: return (name: .C, octave: octave) + case 1: return (name: .C_sharp, octave: octave) + case 2: return (name: .D, octave: octave) + case 3: return (name: .D_sharp, octave: octave) + case 4: return (name: .E, octave: octave) + case 5: return (name: .F, octave: octave) + case 6: return (name: .F_sharp, octave: octave) + case 7: return (name: .G, octave: octave) + case 8: return (name: .G_sharp, octave: octave) + default: + // should never happen + assertionFailure("Modulus is broken.") + return (name: .C, octave: -2) + } + } + } } diff --git a/Sources/MIDIKit/Note/MIDI Note.swift b/Sources/MIDIKit/Note/MIDI Note.swift index 7d0f7999f3..f41d134ce0 100644 --- a/Sources/MIDIKit/Note/MIDI Note.swift +++ b/Sources/MIDIKit/Note/MIDI Note.swift @@ -73,9 +73,7 @@ public extension MIDI { public mutating func setNoteNumber(_ source: Name, octave: Int) -> Bool { - let rootValue = 0 - - let noteNum = rootValue + ((octave + 2) * 12) + source.scaleOffset + let noteNum = ((octave + 2) * 12) + source.scaleOffset guard let uint7 = MIDI.UInt7(exactly: noteNum) else { return false } @@ -158,6 +156,16 @@ public extension MIDI { } + /// Get the MIDI note name enum case. + public var name: Name { + Name.convert(noteNumber: number).name + } + + /// Get the MIDI note name enum case. + public var octave: Int { + Name.convert(noteNumber: number).octave + } + /// Get the MIDI note name string (ie: "A-2" "C#6") public func stringValue( respellSharpAsFlat: Bool = false, @@ -251,7 +259,7 @@ extension MIDI.Note: Strideable { public func advanced(by n: Int) -> Self { let val = (number.intValue + n) - .clamped(to: MIDI.NoteRange.all.lowerBound.intValue ... MIDI.NoteRange.all.upperBound.intValue) + .clamped(to: MIDI.NoteNumberRange.all.lowerBound.intValue ... MIDI.NoteNumberRange.all.upperBound.intValue) return Self(number: val) ?? .init() @@ -262,8 +270,7 @@ extension MIDI.Note: Strideable { public extension MIDI.Note { /// Returns an array of all 128 MIDI notes. - static let allNotes: [Self] = - MIDI.NoteRange.all.map { Self($0) } + static let allNotes: [Self] = MIDI.NoteRange.all.map { $0 } } diff --git a/Tests/MIDIKitTests/Note/MIDI Note Tests.swift b/Tests/MIDIKitTests/Note/MIDI Note Tests.swift index f7349e546e..6a8b3ca008 100644 --- a/Tests/MIDIKitTests/Note/MIDI Note Tests.swift +++ b/Tests/MIDIKitTests/Note/MIDI Note Tests.swift @@ -213,6 +213,20 @@ final class NoteTests: XCTestCase { XCTAssertEqual(MIDI.Note(.G_sharp, octave: 8)?.number, nil) } + func testNoteName() { + XCTAssertEqual(MIDI.Note(0).name, .C) + XCTAssertEqual(MIDI.Note(0).octave, -2) + + XCTAssertEqual(MIDI.Note(59).name, .B) + XCTAssertEqual(MIDI.Note(59).octave, 2) + + XCTAssertEqual(MIDI.Note(60).name, .C) + XCTAssertEqual(MIDI.Note(60).octave, 3) + + XCTAssertEqual(MIDI.Note(127).name, .G) + XCTAssertEqual(MIDI.Note(127).octave, 8) + } + func testPianoKeyType_WhiteKeys() { // generate white keys @@ -232,7 +246,7 @@ final class NoteTests: XCTestCase { // test white keys XCTAssertEqual(whiteKeyNotes.count, 75) - XCTAssert(whiteKeyNotes.allSatisfy { $0.pianoKey == .white }) + XCTAssert(whiteKeyNotes.allSatisfy { !$0.isSharp }) } @@ -255,7 +269,7 @@ final class NoteTests: XCTestCase { // test black keys XCTAssertEqual(blackKeyNotes.count, 53) - XCTAssert(blackKeyNotes.allSatisfy { $0.pianoKey == .black }) + XCTAssert(blackKeyNotes.allSatisfy { $0.isSharp }) }