├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── MIDI │ ├── MIDI.swift │ ├── Pitch+Frequency.swift │ ├── Pitch+Note.swift │ └── Pitch.swift └── Music │ ├── Note.swift │ └── Octave.swift ├── Tests ├── MIDI │ ├── Frequency │ │ └── main.swift │ ├── MIDI │ │ └── main.swift │ ├── Pitch │ │ └── main.swift │ └── PitchNote │ │ └── main.swift └── Music │ ├── Note │ └── main.swift │ └── Octave │ └── main.swift └── run_tests /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | /Package.resolved 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Music", 6 | platforms: [ 7 | .iOS(.v16), 8 | .macOS(.v13), 9 | ], 10 | products: [ 11 | .library( 12 | name: "Music", 13 | targets: ["Music"]), 14 | .library( 15 | name: "MIDI", 16 | targets: ["MIDI"]), 17 | ], 18 | dependencies: [ 19 | .package(name: "Test"), 20 | ], 21 | targets: [ 22 | .target( 23 | name: "Music", 24 | swiftSettings: swift6), 25 | .target( 26 | name: "MIDI", 27 | dependencies: [ 28 | .target(name: "Music") 29 | ], 30 | swiftSettings: swift6), 31 | ] 32 | ) 33 | 34 | let swift6: [SwiftSetting] = [ 35 | .enableUpcomingFeature("ConciseMagicFile"), 36 | .enableUpcomingFeature("ForwardTrailingClosures"), 37 | .enableUpcomingFeature("ExistentialAny"), 38 | .enableUpcomingFeature("StrictConcurrency"), 39 | .enableUpcomingFeature("ImplicitOpenExistentials"), 40 | .enableUpcomingFeature("BareSlashRegexLiterals"), 41 | ] 42 | 43 | // MARK: - tests 44 | 45 | testTarget("MIDI") { test in 46 | test("Frequency") 47 | test("MIDI") 48 | test("Pitch") 49 | test("PitchNote") 50 | } 51 | 52 | testTarget("Music") { test in 53 | test("Note") 54 | test("Octave") 55 | } 56 | 57 | func testTarget(_ target: String, task: ((String) -> Void) -> Void) { 58 | task { test in addTest(target: target, name: test) } 59 | } 60 | 61 | func addTest(target: String, name: String) { 62 | package.targets.append( 63 | .executableTarget( 64 | name: "Tests/\(target)/\(name)", 65 | dependencies: [ 66 | .target(name: "Music"), 67 | .target(name: "MIDI"), 68 | .product(name: "Test", package: "test"), 69 | ], 70 | path: "Tests/\(target)/\(name)", 71 | swiftSettings: swift6)) 72 | } 73 | 74 | // MARK: - custom package source 75 | 76 | #if canImport(ObjectiveC) 77 | import Darwin.C 78 | #else 79 | import Glibc 80 | #endif 81 | 82 | extension Package.Dependency { 83 | enum Source: String { 84 | case local, swiftcore, github 85 | 86 | static var `default`: Self { .github } 87 | 88 | var baseUrl: String { 89 | switch self { 90 | case .local: return "../" 91 | case .swiftcore: return "https://swiftstack.io/" 92 | case .github: return "https://github.com/swiftstack/" 93 | } 94 | } 95 | 96 | func url(for name: String) -> String { 97 | return self == .local 98 | ? baseUrl + name.lowercased() 99 | : baseUrl + name.lowercased() + ".git" 100 | } 101 | } 102 | 103 | static func package(name: String) -> Package.Dependency { 104 | guard let pointer = getenv("SWIFTSTACK") else { 105 | return .package(name: name, source: .default) 106 | } 107 | guard let source = Source(rawValue: String(cString: pointer)) else { 108 | fatalError("Invalid source. Use local, swiftcore or github") 109 | } 110 | return .package(name: name, source: source) 111 | } 112 | 113 | static func package(name: String, source: Source) -> Package.Dependency { 114 | return source == .local 115 | ? .package(name: name, path: source.url(for: name)) 116 | : .package(url: source.url(for: name), branch: "dev") 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Music 2 | 3 | Note, Pitch, Pitch <-> Frequency conversions, [WIP] YIN frequency estimator 4 | 5 | ```swift 6 | .package(url: "https://github.com/swiftstack/music.git", .branch("dev")) 7 | ``` 8 | -------------------------------------------------------------------------------- /Sources/MIDI/MIDI.swift: -------------------------------------------------------------------------------- 1 | @_exported import Music 2 | 3 | public enum MIDI { 4 | public struct Number: Equatable { 5 | public var value: Int 6 | 7 | public static let min: Number = 0 8 | public static let max: Number = 127 9 | /// A4 10 | public static let standart: Number = 69 11 | 12 | public init?(_ value: Int) { 13 | // magic numbers to avoid recursion 14 | guard value >= 0 && value <= 127 else { 15 | return nil 16 | } 17 | self.value = value 18 | } 19 | } 20 | } 21 | 22 | extension MIDI.Number: CustomStringConvertible { 23 | public var description: String { 24 | return String(describing: value) 25 | } 26 | } 27 | 28 | extension MIDI.Number: ExpressibleByIntegerLiteral { 29 | public init(integerLiteral value: Int) { 30 | guard let number = MIDI.Number(value) else { 31 | fatalError("Invalid MIDI number: \(value)") 32 | } 33 | self = number 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/MIDI/Pitch+Frequency.swift: -------------------------------------------------------------------------------- 1 | import Music 2 | 3 | extension Pitch { 4 | public struct Frequency: Equatable { 5 | public var value: Double 6 | 7 | public init?(_ value: Double) { 8 | guard value >= Frequency.min && value <= Frequency.max else { 9 | return nil 10 | } 11 | self.value = value 12 | } 13 | 14 | public static var standart: Double = 440.0 15 | 16 | public static let min: Double = 8.175798915643707 17 | public static let max: Double = 12543.853951415975 18 | } 19 | } 20 | 21 | extension Pitch { 22 | public var frequency: Frequency { 23 | let semitones = Double(Octave.semitonesCount) 24 | let steps = number.value - MIDI.Number.standart.value 25 | let power = Double(steps) / Double(semitones) 26 | return Frequency(_exp2(power) * Frequency.standart)! 27 | } 28 | 29 | public init(from frequency: Frequency) { 30 | let semitones = Double(Octave.semitonesCount) 31 | let standart = Frequency.standart 32 | let steps = (semitones * _log2(frequency.value / standart)).rounded() 33 | precondition(steps >= -69 && steps <= 58) 34 | self.init(halfStepsFromStandard: Int(steps))! 35 | if self.frequency != frequency { 36 | self.offset = self.frequency.interval(to: frequency) 37 | } 38 | } 39 | } 40 | 41 | extension Pitch.Frequency { 42 | public func interval(to frequency: Pitch.Frequency) -> Pitch.Cents { 43 | return Pitch.Frequency.interval(self, frequency) 44 | } 45 | 46 | public static func interval( 47 | _ frequency1: Pitch.Frequency, 48 | _ frequency2: Pitch.Frequency 49 | ) -> Pitch.Cents { 50 | return Pitch.Cents(1200.0 * _log2(frequency2.value / frequency1.value)) 51 | } 52 | } 53 | 54 | extension Pitch.Frequency: Comparable { 55 | public static func < (lhs: Pitch.Frequency, rhs: Pitch.Frequency) -> Bool { 56 | return lhs.value < rhs.value 57 | } 58 | } 59 | 60 | extension Pitch.Frequency: ExpressibleByFloatLiteral { 61 | public init(floatLiteral value: Double) { 62 | guard let frequency = Pitch.Frequency(value) else { 63 | fatalError("invalid frequency") 64 | } 65 | self = frequency 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/MIDI/Pitch+Note.swift: -------------------------------------------------------------------------------- 1 | import Music 2 | 3 | extension Pitch { 4 | public var note: Note.Pitch { 5 | return Note.Pitch(from: self) 6 | } 7 | 8 | public init?( 9 | letter: Note.Letter, 10 | accidental: Note.Accidental = .default, 11 | octave: Octave = .default, 12 | offset: Cents = .default 13 | ) { 14 | let base = Octave.semitonesCount * (octave.rawValue + 1) 15 | var rawNumber = base + letter.number 16 | switch accidental { 17 | case .natural: break 18 | case .sharp: rawNumber += 1 19 | case .flat: rawNumber -= 1 20 | } 21 | guard let number = MIDI.Number(rawNumber) else { 22 | return nil 23 | } 24 | self.number = number 25 | self.offset = offset 26 | } 27 | 28 | public init?( 29 | name: Note.Name, 30 | octave: Octave = .default, 31 | offset: Cents = .default 32 | ) { 33 | self.init( 34 | letter: name.letter, 35 | accidental: name.accidental, 36 | octave: octave, 37 | offset: offset) 38 | } 39 | 40 | public init?( 41 | pitch: Note.Pitch, 42 | offset: Cents = .default 43 | ) { 44 | self.init( 45 | name: pitch.name, 46 | octave: pitch.octave, 47 | offset: offset) 48 | } 49 | } 50 | 51 | public enum SemitoneRepresentation { 52 | case sharp, flat 53 | public static let `default`: SemitoneRepresentation = .sharp 54 | } 55 | 56 | extension Note.Pitch { 57 | public init( 58 | from pitch: Pitch, 59 | semitoneRepresentation: SemitoneRepresentation = .default 60 | ) { 61 | let letter = Note.Letter( 62 | for: pitch.number, 63 | semitoneRepresentation: semitoneRepresentation) 64 | 65 | let accidental = Note.Accidental( 66 | for: pitch.number, 67 | semitoneRepresentation: semitoneRepresentation) 68 | 69 | self.init( 70 | name: .init(letter: letter, accidental: accidental), 71 | octave: .init(pitch.number)) 72 | } 73 | } 74 | 75 | extension Note.Letter { 76 | public init( 77 | for number: MIDI.Number, 78 | semitoneRepresentation: SemitoneRepresentation = .default 79 | ) { 80 | let index = number.value % Octave.semitonesCount 81 | switch number.isFlat { 82 | case true: 83 | switch semitoneRepresentation { 84 | case .sharp: self = Note.Letter(index - 1) 85 | case .flat: self = Note.Letter(index + 1) 86 | } 87 | case false: 88 | self = Note.Letter(index) 89 | } 90 | } 91 | } 92 | 93 | extension Note.Accidental { 94 | public init( 95 | for number: MIDI.Number, 96 | semitoneRepresentation: SemitoneRepresentation = .default 97 | ) { 98 | switch number.isFlat { 99 | case true: 100 | switch semitoneRepresentation { 101 | case .sharp: self = .sharp 102 | case .flat: self = .flat 103 | } 104 | case false: 105 | self = .natural 106 | } 107 | } 108 | } 109 | 110 | extension Octave { 111 | public init(_ number: MIDI.Number) { 112 | self = Octave(number.value / Octave.semitonesCount - 1)! 113 | } 114 | } 115 | 116 | extension MIDI.Number { 117 | var isFlat: Bool { 118 | switch value % Octave.semitonesCount { 119 | case 1, 3, 6, 8, 10: return true 120 | default: return false 121 | } 122 | } 123 | } 124 | 125 | extension Note.Letter { 126 | fileprivate var number: Int { 127 | switch self { 128 | case .c: return 0 129 | case .d: return 2 130 | case .e: return 4 131 | case .f: return 5 132 | case .g: return 7 133 | case .a: return 9 134 | case .b: return 11 135 | } 136 | } 137 | 138 | fileprivate init(_ number: Int) { 139 | assert(number >= 0 && number < 12) 140 | switch number { 141 | case 0: self = .c 142 | case 2: self = .d 143 | case 4: self = .e 144 | case 5: self = .f 145 | case 7: self = .g 146 | case 9: self = .a 147 | case 11: self = .b 148 | default: fatalError("unreachable") 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/MIDI/Pitch.swift: -------------------------------------------------------------------------------- 1 | import Music 2 | 3 | public struct Pitch: Equatable { 4 | public var number: MIDI.Number 5 | public var offset: Cents 6 | 7 | public init(number: MIDI.Number, offset: Cents = .default) { 8 | self.number = number 9 | self.offset = offset 10 | } 11 | 12 | public struct Cents: Equatable { 13 | public var value: Double 14 | 15 | public init(_ value: Double) { 16 | self.value = value 17 | } 18 | 19 | public static let `default`: Cents = 0.0 20 | public static let semitone: Cents = 100.0 21 | } 22 | } 23 | 24 | extension Pitch { 25 | public init?(halfStepsFromStandard halfSteps: Int) { 26 | let standart = MIDI.Number.standart.value 27 | guard let number = MIDI.Number(standart + halfSteps) else { 28 | return nil 29 | } 30 | self.init(number: number) 31 | } 32 | 33 | public func advanced(by halfSteps: Int) -> Pitch? { 34 | guard let number = MIDI.Number(number.value + halfSteps) else { 35 | return nil 36 | } 37 | return Pitch(number: number) 38 | } 39 | } 40 | 41 | extension Pitch.Cents: ExpressibleByFloatLiteral { 42 | public init(floatLiteral value: Double) { 43 | self.value = value 44 | } 45 | } 46 | 47 | extension Pitch: CustomStringConvertible { 48 | public var description: String { 49 | let plus = offset.value >= 0.0 ? "+" : "" 50 | let integer = Int(offset.value) 51 | return "\(Note.Pitch(from: self))\(plus)\(integer)" 52 | } 53 | 54 | public var debugDescription: String { 55 | let plus = offset.value >= 0.0 ? "+" : "" 56 | let integer = Int(offset.value) 57 | let fract = Int(Swift.abs(offset.value) * 100) % 100 58 | return "\(Note.Pitch(from: self))\(plus)\(integer).\(fract)" 59 | } 60 | } 61 | 62 | extension Pitch.Cents: CustomStringConvertible { 63 | public var description: String { 64 | return String(describing: value) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Music/Note.swift: -------------------------------------------------------------------------------- 1 | public struct Note { 2 | public let pitch: Pitch 3 | public let duration: Duration 4 | 5 | public struct Pitch { 6 | public var name: Name 7 | public var octave: Octave 8 | 9 | public init( 10 | name: Name, 11 | octave: Octave = .default 12 | ) { 13 | self.name = name 14 | self.octave = octave 15 | } 16 | } 17 | 18 | public struct Name { 19 | public var letter: Letter 20 | public var accidental: Accidental 21 | 22 | public init(letter: Letter, accidental: Accidental = .default) { 23 | self.letter = letter 24 | self.accidental = accidental 25 | } 26 | } 27 | 28 | public enum Letter: String { 29 | case c = "C" 30 | case d = "D" 31 | case e = "E" 32 | case f = "F" 33 | case g = "G" 34 | case a = "A" 35 | case b = "B" 36 | } 37 | 38 | public enum Accidental { 39 | case sharp 40 | case flat 41 | case natural 42 | 43 | public static let `default`: Accidental = .natural 44 | } 45 | 46 | public struct Duration { 47 | public let size: Size 48 | public let dots: Dots 49 | 50 | public static let `default` = Duration(size: .whole, dots: .none) 51 | 52 | public enum Size { 53 | case large 54 | case long 55 | case doubleWhole 56 | case whole 57 | case half 58 | case quarter 59 | case eighth 60 | case sixteenth 61 | case thirtySecond 62 | case sixtyFourth 63 | case hundredTwentyEighth 64 | case twoHundredFiftySixth 65 | } 66 | 67 | public enum Dots: String { 68 | case none, one, two, three 69 | } 70 | } 71 | 72 | public init(pitch: Pitch, duration: Duration = .default) { 73 | self.pitch = pitch 74 | self.duration = duration 75 | } 76 | } 77 | 78 | extension Note { 79 | public init( 80 | name: Name, 81 | octave: Octave = .default, 82 | duration: Duration = .default 83 | ) { 84 | self.init( 85 | pitch: Pitch( 86 | name: name, 87 | octave: octave), 88 | duration: duration) 89 | } 90 | 91 | public init( 92 | letter: Letter, 93 | accidental: Accidental = .default, 94 | octave: Octave = .default, 95 | duration: Duration = .default 96 | ) { 97 | self.init( 98 | pitch: Pitch( 99 | name: Name(letter: letter, accidental: accidental), 100 | octave: octave), 101 | duration: duration) 102 | } 103 | } 104 | 105 | extension Note.Name { 106 | public static let c = Note.Name(letter: .c, accidental: .natural) 107 | public static let cSharp = Note.Name(letter: .c, accidental: .sharp) 108 | public static let dFlat = Note.Name(letter: .d, accidental: .flat) 109 | public static let d = Note.Name(letter: .d, accidental: .natural) 110 | public static let dSharp = Note.Name(letter: .d, accidental: .sharp) 111 | public static let eFlat = Note.Name(letter: .e, accidental: .flat) 112 | public static let e = Note.Name(letter: .e, accidental: .natural) 113 | public static let f = Note.Name(letter: .f, accidental: .natural) 114 | public static let fSharp = Note.Name(letter: .f, accidental: .sharp) 115 | public static let gFlat = Note.Name(letter: .g, accidental: .flat) 116 | public static let g = Note.Name(letter: .g, accidental: .natural) 117 | public static let gSharp = Note.Name(letter: .g, accidental: .sharp) 118 | public static let aFlat = Note.Name(letter: .a, accidental: .flat) 119 | public static let a = Note.Name(letter: .a, accidental: .natural) 120 | public static let aSharp = Note.Name(letter: .a, accidental: .sharp) 121 | public static let bFlat = Note.Name(letter: .b, accidental: .flat) 122 | public static let b = Note.Name(letter: .b, accidental: .natural) 123 | } 124 | 125 | extension Note.Letter { 126 | public var next: Note.Letter { 127 | switch self { 128 | case .c: return .d 129 | case .d: return .e 130 | case .e: return .f 131 | case .f: return .g 132 | case .g: return .a 133 | case .a: return .b 134 | case .b: return .c 135 | } 136 | } 137 | 138 | public var prev: Note.Letter { 139 | switch self { 140 | case .c: return .b 141 | case .d: return .c 142 | case .e: return .d 143 | case .f: return .e 144 | case .g: return .f 145 | case .a: return .g 146 | case .b: return .a 147 | } 148 | } 149 | } 150 | 151 | extension Note: CustomStringConvertible { 152 | public var description: String { 153 | return "\(pitch)" 154 | } 155 | } 156 | 157 | extension Note.Pitch: CustomStringConvertible { 158 | public var description: String { 159 | return "\(name)\(octave)" 160 | } 161 | } 162 | 163 | extension Note.Name: CustomStringConvertible { 164 | public var description: String { 165 | return "\(letter)\(accidental)" 166 | } 167 | } 168 | 169 | extension Note.Letter: CustomStringConvertible { 170 | public var description: String { 171 | return rawValue 172 | } 173 | } 174 | 175 | extension Note.Accidental: CustomStringConvertible { 176 | public var description: String { 177 | switch self { 178 | case .sharp: return "#" 179 | case .flat: return "♭" 180 | case .natural: return "" 181 | } 182 | } 183 | } 184 | 185 | extension Note.Duration.Size: CustomStringConvertible { 186 | public var description: String { 187 | switch self { 188 | case .large: return "large" 189 | case .long: return "long" 190 | case .doubleWhole: return "double whole" 191 | case .whole: return "whole" 192 | case .half: return "half" 193 | case .quarter: return "quater" 194 | case .eighth: return "eighth" 195 | case .sixteenth: return "sixteenth" 196 | case .thirtySecond: return "thirty-second" 197 | case .sixtyFourth: return "sixty-fourth" 198 | case .hundredTwentyEighth: return "hundred twenty-eighth" 199 | case .twoHundredFiftySixth: return "two hundred fifty-sixth" 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/Music/Octave.swift: -------------------------------------------------------------------------------- 1 | public enum Octave: Int { 2 | case minusOne = -1 3 | case zero = 0 4 | case one 5 | case two 6 | case three 7 | case four 8 | case five 9 | case six 10 | case seven 11 | case eight 12 | case nine 13 | 14 | public static let `default`: Octave = .four 15 | 16 | public static let min: Octave = .minusOne 17 | public static let max: Octave = .nine 18 | 19 | public static let semitonesCount: Int = 12 20 | } 21 | 22 | extension Octave { 23 | public init?(_ number: Int) { 24 | guard number >= -1 && number <= 9 else { 25 | return nil 26 | } 27 | self.init(rawValue: number)! 28 | } 29 | } 30 | 31 | extension Octave: CustomStringConvertible { 32 | public var description: String { 33 | return "\(rawValue)" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/MIDI/Frequency/main.swift: -------------------------------------------------------------------------------- 1 | import Test 2 | @testable import MIDI 3 | 4 | test("pitch from frequency") { 5 | expect(Pitch(from: 880.0).debugDescription == "A5+0.0") 6 | expect(Pitch(from: 174.61411571650194).debugDescription == "F3+0.0") 7 | } 8 | 9 | test("pitch to frequency") { 10 | let a5 = Pitch(name: .a, octave: .five, offset: 0.0) 11 | expect(a5?.frequency == 880.0) 12 | let f3 = Pitch(name: .f, octave: .three, offset: 0.0) 13 | expect(f3?.frequency == 174.61411571650194) 14 | } 15 | 16 | test("pitch offset") { 17 | #if os(Linux) 18 | expect(Pitch(from: 444.0).offset == 15.667383390535543) 19 | #else 20 | expect(Pitch(from: 444.0).offset == 15.66738339053554) 21 | #endif 22 | expect(Pitch(from: 444.0).debugDescription == "A4+15.66") 23 | 24 | expect(Pitch(from: 439.0).offset == -3.939100787161778) 25 | expect(Pitch(from: 439.0).debugDescription == "A4-3.93") 26 | } 27 | 28 | await run() 29 | -------------------------------------------------------------------------------- /Tests/MIDI/MIDI/main.swift: -------------------------------------------------------------------------------- 1 | import Test 2 | @testable import MIDI 3 | 4 | test("midi number") { 5 | expect(MIDI.Number(Int(0)) != nil) 6 | expect(MIDI.Number(Int(127)) != nil) 7 | expect(MIDI.Number(Int(-1)) == nil) 8 | expect(MIDI.Number(Int(128)) == nil) 9 | } 10 | 11 | await run() 12 | -------------------------------------------------------------------------------- /Tests/MIDI/Pitch/main.swift: -------------------------------------------------------------------------------- 1 | import Test 2 | @testable import MIDI 3 | 4 | test("pitch") { 5 | let pitch = Pitch(number: 69) 6 | expect(pitch.number == 69) 7 | } 8 | 9 | test("pitch description") { 10 | let a4 = Pitch(number: 69) 11 | expect(a4.description == "A4+0") 12 | expect(a4.debugDescription == "A4+0.0") 13 | 14 | let gSharp4 = Pitch(number: 68, offset: 1.34) 15 | expect(gSharp4.description == "G#4+1") 16 | expect(gSharp4.debugDescription == "G#4+1.34") 17 | } 18 | 19 | test("pitch from note") { 20 | let pitch = Pitch( 21 | letter: .a, 22 | accidental: .flat, 23 | octave: .four, 24 | offset: 0.0) 25 | 26 | expect(pitch?.number == 68) 27 | } 28 | 29 | await run() 30 | -------------------------------------------------------------------------------- /Tests/MIDI/PitchNote/main.swift: -------------------------------------------------------------------------------- 1 | import Test 2 | @testable import MIDI 3 | 4 | test("note from pitch natural") { 5 | let pitch = Pitch(number: 69) 6 | let note = Note.Pitch(from: pitch) 7 | 8 | expect(note.name.letter == .a) 9 | expect(note.name.accidental == .natural) 10 | expect(note.octave == .four) 11 | } 12 | 13 | test("note from pitch sharp") { 14 | let pitch = Pitch(number: 70) 15 | let note = Note.Pitch(from: pitch) 16 | 17 | expect(note.name.letter == .a) 18 | expect(note.name.accidental == .sharp) 19 | expect(note.octave == .four) 20 | } 21 | 22 | test("note from pitch flat") { 23 | let pitch = Pitch(number: 70) 24 | let note = Note.Pitch(from: pitch, semitoneRepresentation: .flat) 25 | 26 | expect(note.name.letter == .b) 27 | expect(note.name.accidental == .flat) 28 | expect(note.octave == .four) 29 | } 30 | 31 | await run() 32 | -------------------------------------------------------------------------------- /Tests/Music/Note/main.swift: -------------------------------------------------------------------------------- 1 | import Test 2 | @testable import Music 3 | 4 | test("note name") { 5 | let name = Note.Name(letter: .a, accidental: .natural) 6 | expect(name.letter == .a) 7 | expect(name.accidental == .natural) 8 | } 9 | 10 | test("note pitch") { 11 | let pitch = Note.Pitch(name: .a, octave: .four) 12 | expect(pitch.name.letter == .a) 13 | expect(pitch.name.accidental == .natural) 14 | expect(pitch.octave == .four) 15 | } 16 | 17 | test("note pitch description") { 18 | let a4 = Note.Pitch(name: .a, octave: .four) 19 | expect(a4.description == "A4") 20 | 21 | let gSharp4 = Note.Pitch(name: .gSharp, octave: .three) 22 | expect(gSharp4.description == "G#3") 23 | } 24 | 25 | test("note") { 26 | let note = Note(name: .a, octave: .four) 27 | expect(note.pitch.name.letter == .a) 28 | expect(note.pitch.octave == .four) 29 | } 30 | 31 | test("note description") { 32 | let a4 = Note(letter: .a, octave: .four) 33 | expect(a4.description == "A4") 34 | 35 | let gSharp4 = Note(letter: .g, accidental: .sharp, octave: .three) 36 | expect(gSharp4.description == "G#3") 37 | } 38 | 39 | await run() 40 | -------------------------------------------------------------------------------- /Tests/Music/Octave/main.swift: -------------------------------------------------------------------------------- 1 | import Test 2 | @testable import Music 3 | 4 | test("octave") { 5 | expect(Octave(-1) == .minusOne) 6 | expect(Octave(0) == .zero) 7 | expect(Octave(1) == .one) 8 | expect(Octave(2) == .two) 9 | expect(Octave(3) == .three) 10 | expect(Octave(4) == .four) 11 | expect(Octave(5) == .five) 12 | expect(Octave(6) == .six) 13 | expect(Octave(7) == .seven) 14 | expect(Octave(8) == .eight) 15 | expect(Octave(9) == .nine) 16 | expect(Octave(-2) == nil) 17 | expect(Octave(10) == nil) 18 | } 19 | 20 | await run() 21 | -------------------------------------------------------------------------------- /run_tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | swift build 6 | 7 | export DYLD_LIBRARY_PATH=/Library/Developer/Toolchains/swift-latest.xctoolchain/usr/lib/swift/macosx 8 | 9 | .build/debug/Tests/MIDI/Frequency 10 | .build/debug/Tests/MIDI/MIDI 11 | .build/debug/Tests/MIDI/Pitch 12 | .build/debug/Tests/MIDI/PitchNote 13 | 14 | .build/debug/Tests/Music/Note 15 | .build/debug/Tests/Music/Octave 16 | --------------------------------------------------------------------------------