├── .github ├── CODEOWNERS └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Microtonality │ ├── TuningTable+Brun.swift │ ├── TuningTable+CombinationProductSet.swift │ ├── TuningTable+EqualTemperament.swift │ ├── TuningTable+NorthIndianRaga.swift │ ├── TuningTable+RecurrenceRelation.swift │ ├── TuningTable+Scala.swift │ ├── TuningTable+Wilson.swift │ ├── TuningTable.swift │ └── TuningTableBase.swift ├── Tests └── MicrotonalityTests │ └── MicrotonalityTests.swift └── images └── synthone.jpg /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # SoundpipeAudioKit Code Owners File 2 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | # Primary Owners 5 | * @aure @marcussatellite -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | swift_test: 12 | name: Test 13 | uses: AudioKit/ci/.github/workflows/swift_test.yml@main 14 | with: 15 | scheme: Microtonality 16 | platforms: iOS macOS tvOS watchOS Linux 17 | swift-versions: 5.3 5.4 5.5 5.6 18 | 19 | # Send notification to Discord on failure. 20 | send_notification: 21 | name: Send Notification 22 | uses: AudioKit/ci/.github/workflows/send_notification.yml@main 23 | needs: [swift_test] 24 | if: ${{ failure() && github.ref == 'refs/heads/main' }} 25 | secrets: inherit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 AudioKit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Microtonality", 8 | products: [.library(name: "Microtonality", targets: ["Microtonality"])], 9 | targets: [ 10 | .target(name: "Microtonality"), 11 | .testTarget(name: "MicrotonalityTests", dependencies: ["Microtonality"]), 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Microtonality 4 | 5 | [![Build Status](https://github.com/AudioKit/Microtonality/workflows/CI/badge.svg)](https://github.com/AudioKit/Microtonality/actions?query=workflow%3ACI) 6 | [![License](https://img.shields.io/github/license/AudioKit/Microtonality)](https://github.com/AudioKit/Microtonality/blob/main/LICENSE) 7 | [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) 8 | [![Twitter Follow](https://img.shields.io/twitter/follow/AudioKitPro.svg?style=social)](https://twitter.com/AudioKitPro) 9 | 10 | https://user-images.githubusercontent.com/13122/188533099-b09ced85-089c-4a86-8313-46c7d97819c9.mov 11 | 12 |
13 | 14 | Tuning tables developed by Marcus Hobbs and used in the AudioKit Synth One iOS app. 15 | 16 | ## Installation 17 | 18 | Install with Swift Package Manager. 19 | 20 | ## Documentation 21 | 22 | - [TuningTableETNN](https://audiokit.io/Microtonality/documentation/microtonality/tuningtableetnn): 23 | helper object to simulate a Swift tuple for ObjC interoperability 24 | - [TuningTableDelta12ET](https://audiokit.io/Microtonality/documentation/microtonality/tuningtabledelta12et): 25 | helper object to simulate a Swift tuple for ObjC interoperability 26 | - [TuningTable](https://audiokit.io/Microtonality/documentation/microtonality/tuningtable): 27 | TuningTable provides high-level methods to create musically useful tuning tables 28 | - [TuningTableBase](https://audiokit.io/Microtonality/documentation/microtonality/tuningtablebase): 29 | TuningTableBase provides low-level methods for creating 30 | arbitrary mappings of midi note numbers to musical frequencies 31 | The default behavior is "12-tone equal temperament" so 32 | we can integrate in non-microtonal settings with backwards compatibility 33 | -------------------------------------------------------------------------------- /Sources/Microtonality/TuningTable+Brun.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ 2 | 3 | import Foundation 4 | 5 | extension TuningTable { 6 | // Viggo Brun algorithm 7 | // return (numerator, denominator) approximation to generator after level iterations 8 | fileprivate static func brunLevel_0_1_1_0(level l: Int, generator g: Double) -> (numerator: Int, denominator: Int) { 9 | var zn = 0, zd = 1, infn = 1, infd = 0, fn = 0, fd = 0 10 | 11 | for _ in 0 ..< l { 12 | fn = zn + infn 13 | fd = zd + infd 14 | if g > Double(fn) / Double(fd) { 15 | zn = fn 16 | zd = fd 17 | } else { 18 | infn = fn 19 | infd = fd 20 | } 21 | } 22 | return (numerator: fn, denominator: fd) 23 | } 24 | 25 | /// Creates a "Nested 2-interval pattern", or "Moment of Symmetry" 26 | /// From Erv Wilson. See http://anaphoria.com/wilsonintroMOS.html 27 | /// 28 | /// - parameter gInput: A Double on [0, 1] 29 | /// - parameter lInput: An Int on [0, 7] 30 | /// - parameter mInput: The mode of the scale...degrees are normalized by the frequency at this index 31 | /// - returns: Number of notes per octave 32 | /// 33 | public func momentOfSymmetry(generator gInput: Double = 7.0 / 12.0, 34 | level lInput: Int = 5, 35 | murchana mInput: Int = 0) -> Int 36 | { 37 | // CLAMP 38 | let g = (gInput > 1.0) ? 1.0 : ((gInput < 0) ? 0.0 : gInput) 39 | let l = (lInput > 7) ? 7 : ((lInput < 0) ? 0 : lInput) 40 | let d = TuningTable.brunLevel_0_1_1_0(level: l, generator: g) 41 | 42 | // NPO number of notes per octave 43 | let den = d.denominator 44 | var f = [Frequency]() 45 | for i in 0 ..< den { 46 | let p = exp2((Double(i) * g).truncatingRemainder(dividingBy: 1.0)) 47 | f.append(Frequency(p)) 48 | } 49 | 50 | // apply murchana then octave reduce 51 | let m = (mInput > den) ? (den - 1) : ((mInput < 0) ? 0 : mInput) 52 | let murchana = f[m] 53 | f = f.map { (frequency: Frequency) -> Frequency in 54 | // murchana = index of modulation == normalize by this scale degree 55 | var ff = frequency / murchana 56 | // octave reduce. Assumes octave = 2 57 | while ff < 1.0 { 58 | ff *= 2.0 59 | } 60 | while ff >= 2.0 { 61 | ff /= 2.0 62 | } 63 | return ff 64 | } 65 | f.sort() 66 | 67 | // update tuning table 68 | _ = tuningTable(fromFrequencies: f) 69 | 70 | return den 71 | } 72 | 73 | // Examples: 74 | // 75 | // 12ET: 76 | // PolyphonicNode.tuningTable.momentOfSymmetry(generator: 0.583333, level: 6) -> 12 77 | 78 | // 9-tone scale 79 | // PolyphonicNode.tuningTable.momentOfSymmetry(generator: 0.238186, level: 5) -> 9 80 | 81 | // 9-tone scale 82 | // PolyphonicNode.tuningTable.momentOfSymmetry(generator: 0.264100, level: 5) -> 9 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Microtonality/TuningTable+CombinationProductSet.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ 2 | 3 | public extension TuningTable { 4 | // swiftlint:disable variable_name 5 | 6 | /// Create a hexany from 4 frequencies (4 choose 2) 7 | /// From Erv Wilson. See http://anaphoria.com/dal.pdf and http://anaphoria.com/hexany.pdf 8 | /// 9 | /// - Parameters: 10 | /// - A: First of the master set of frequencies 11 | /// - B: Second of the master set of frequencies 12 | /// - C: Third of the master set of frequencies 13 | /// - D: Fourth of the master set of frequencies 14 | /// 15 | @discardableResult func hexany(_ A: Frequency, _ B: Frequency, _ C: Frequency, _ D: Frequency) -> Int { 16 | tuningTable(fromFrequencies: [A * B, A * C, A * D, B * C, B * D, C * D]) 17 | return 6 18 | } 19 | 20 | /// Create a major tetrany from 4 frequencies (4 choose 1) 21 | /// 22 | /// - Parameters: 23 | /// - A: First of the master set of frequencies 24 | /// - B: Second of the master set of frequencies 25 | /// - C: Third of the master set of frequencies 26 | /// - D: Fourth of the master set of frequencies 27 | /// 28 | @discardableResult func majorTetrany(_ A: Frequency, 29 | _ B: Frequency, 30 | _ C: Frequency, 31 | _ D: Frequency) -> Int 32 | { 33 | tuningTable(fromFrequencies: [A, B, C, D]) 34 | return 4 35 | } 36 | 37 | /// Create a hexany from 4 frequencies (4 choose 3) 38 | /// 39 | /// - Parameters: 40 | /// - A: First of the master set of frequencies 41 | /// - B: Second of the master set of frequencies 42 | /// - C: Third of the master set of frequencies 43 | /// - D: Fourth of the master set of frequencies 44 | /// 45 | @discardableResult func minorTetrany(_ A: Frequency, 46 | _ B: Frequency, 47 | _ C: Frequency, 48 | _ D: Frequency) -> Int 49 | { 50 | tuningTable(fromFrequencies: [A * B * C, A * B * D, A * C * D, B * C * D]) 51 | return 4 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Microtonality/TuningTable+EqualTemperament.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ 2 | 3 | import Foundation 4 | 5 | public extension TuningTable { 6 | /// Default tuning table is 12ET. 7 | func defaultTuning() -> Int { 8 | return twelveToneEqualTemperament() 9 | } 10 | 11 | /// Create 12-tone equal temperament 12 | func twelveToneEqualTemperament() -> Int { 13 | return equalTemperament(notesPerOctave: 12) 14 | } 15 | 16 | /// Create 31-tone equal temperament 17 | func thirtyOneEqualTemperament() -> Int { 18 | return equalTemperament(notesPerOctave: 31) 19 | } 20 | 21 | /// Create an equal temperament with notesPerOctave 22 | /// 23 | /// - parameter notesPerOctave divides the octave equally by this many steps 24 | /// From Erv Wilson. See http://anaphoria.com/MOSedo.pdf 25 | func equalTemperament(notesPerOctave npo: Int) -> Int { 26 | var nf = [Frequency](repeatElement(1.0, count: npo)) 27 | for i in 0 ..< npo { 28 | nf[i] = Frequency(pow(2.0, Frequency(Frequency(i) / Double(npo)))) 29 | } 30 | _ = tuningTable(fromFrequencies: nf) 31 | return npo 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Microtonality/TuningTable+NorthIndianRaga.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ 2 | 3 | public extension TuningTable { 4 | /// Set tuning to 22 Indian Scale. 5 | /// From Erv Wilson. See http://anaphoria.com/Khiasmos.pdf 6 | @discardableResult func khiasmos22Indian() -> Int { 7 | let masterSet: [Frequency] = [1 / 1, 8 | 256 / 243, 9 | 16 / 15, 10 | 10 / 9, 11 | 9 / 8, 12 | 32 / 27, 13 | 6 / 5, 14 | 5 / 4, 15 | 81 / 64, 16 | 4 / 3, 17 | 27 / 20, 18 | 45 / 32, 19 | 729 / 512, 20 | 3 / 2, 21 | 128 / 81, 22 | 8 / 5, 23 | 5 / 3, 24 | 405 / 240, 25 | 16 / 9, 26 | 9 / 5, 27 | 15 / 8, 28 | 243 / 128] 29 | _ = tuningTable(fromFrequencies: masterSet) 30 | return masterSet.count 31 | } 32 | 33 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 34 | internal static let persianNorthIndianMasterSet: [Frequency] = [1 / 1, 35 | 135 / 128, 36 | 10 / 9, 37 | 9 / 8, 38 | 1215 / 1024, 39 | 5 / 4, 40 | 81 / 64, 41 | 4 / 3, 42 | 45 / 32, 43 | 729 / 512, 44 | 3 / 2, 45 | 405 / 256, 46 | 5 / 3, 47 | 27 / 16, 48 | 16 / 9, 49 | 15 / 8, 50 | 243 / 128] 51 | 52 | fileprivate func helper(_ input: [Int]) -> [Frequency] { 53 | assert(input.count < TuningTable.persianNorthIndianMasterSet.count - 1, "internal error: index out of bounds") 54 | let retVal: [Frequency] = input.map { (number: Int) -> Frequency in 55 | Frequency(TuningTable.persianNorthIndianMasterSet[number]) 56 | } 57 | return retVal 58 | } 59 | 60 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 61 | @discardableResult func presetPersian17NorthIndian00_17() -> Int { 62 | let h = helper([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) 63 | return tuningTable(fromFrequencies: h) 64 | } 65 | 66 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 67 | @discardableResult func presetPersian17NorthIndian01Kalyan() -> Int { 68 | let h = helper([0, 3, 5, 8, 10, 12, 15]) 69 | return tuningTable(fromFrequencies: h) 70 | } 71 | 72 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 73 | @discardableResult func presetPersian17NorthIndian02Bilawal() -> Int { 74 | let h = helper([0, 3, 5, 7, 10, 13, 15]) 75 | return tuningTable(fromFrequencies: h) 76 | } 77 | 78 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 79 | @discardableResult func presetPersian17NorthIndian03Khamaj() -> Int { 80 | let h = helper([0, 3, 5, 7, 10, 12, 14]) 81 | return tuningTable(fromFrequencies: h) 82 | } 83 | 84 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 85 | @discardableResult func presetPersian17NorthIndian04KafiOld() -> Int { 86 | let h = helper([0, 2, 4, 7, 10, 12, 14]) 87 | return tuningTable(fromFrequencies: h) 88 | } 89 | 90 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 91 | @discardableResult func presetPersian17NorthIndian05Kafi() -> Int { 92 | let h = helper([0, 3, 4, 7, 10, 13, 14]) 93 | return tuningTable(fromFrequencies: h) 94 | } 95 | 96 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 97 | @discardableResult func presetPersian17NorthIndian06Asawari() -> Int { 98 | let h = helper([0, 3, 4, 7, 10, 11, 14]) 99 | return tuningTable(fromFrequencies: h) 100 | } 101 | 102 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 103 | @discardableResult func presetPersian17NorthIndian07Bhairavi() -> Int { 104 | let h = helper([0, 1, 4, 7, 10, 11, 14]) 105 | return tuningTable(fromFrequencies: h) 106 | } 107 | 108 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 109 | @discardableResult func presetPersian17NorthIndian08Marwa() -> Int { 110 | let h = helper([0, 1, 5, 8, 10, 12, 15]) 111 | return tuningTable(fromFrequencies: h) 112 | } 113 | 114 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 115 | @discardableResult func presetPersian17NorthIndian09Purvi() -> Int { 116 | let h = helper([0, 1, 5, 8, 10, 11, 15]) 117 | return tuningTable(fromFrequencies: h) 118 | } 119 | 120 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 121 | @discardableResult func presetPersian17NorthIndian10Lalit2() -> Int { 122 | let h = helper([0, 1, 5, 7, 8, 12, 15]) 123 | return tuningTable(fromFrequencies: h) 124 | } 125 | 126 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 127 | @discardableResult func presetPersian17NorthIndian11Todi() -> Int { 128 | let h = helper([0, 1, 4, 8, 10, 11, 15]) 129 | return tuningTable(fromFrequencies: h) 130 | } 131 | 132 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 133 | @discardableResult func presetPersian17NorthIndian12Lalit() -> Int { 134 | let h = helper([0, 1, 5, 7, 8, 11, 15]) 135 | return tuningTable(fromFrequencies: h) 136 | } 137 | 138 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 139 | @discardableResult func presetPersian17NorthIndian13NoName() -> Int { 140 | let h = helper([0, 1, 4, 8, 10, 11, 14]) 141 | return tuningTable(fromFrequencies: h) 142 | } 143 | 144 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 145 | @discardableResult func presetPersian17NorthIndian14AnandBhairav() -> Int { 146 | let h = helper([0, 1, 5, 7, 10, 12, 15]) 147 | tuningTable(fromFrequencies: h) 148 | return h.count 149 | } 150 | 151 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 152 | @discardableResult func presetPersian17NorthIndian15Bhairav() -> Int { 153 | let h = helper([0, 1, 5, 7, 10, 11, 15]) 154 | return tuningTable(fromFrequencies: h) 155 | } 156 | 157 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 158 | @discardableResult func presetPersian17NorthIndian16JogiyaTodi() -> Int { 159 | let h = helper([0, 1, 4, 7, 10, 11, 15]) 160 | return tuningTable(fromFrequencies: h) 161 | } 162 | 163 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 164 | @discardableResult func presetPersian17NorthIndian17Madhubanti() -> Int { 165 | let h = helper([0, 3, 4, 8, 10, 12, 15]) 166 | return tuningTable(fromFrequencies: h) 167 | } 168 | 169 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 170 | @discardableResult func presetPersian17NorthIndian18NatBhairav() -> Int { 171 | let h = helper([0, 3, 5, 7, 10, 11, 15]) 172 | tuningTable(fromFrequencies: h) 173 | return h.count 174 | } 175 | 176 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 177 | @discardableResult func presetPersian17NorthIndian19AhirBhairav() -> Int { 178 | let h = helper([0, 1, 5, 7, 10, 12, 14]) 179 | return tuningTable(fromFrequencies: h) 180 | } 181 | 182 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 183 | @discardableResult func presetPersian17NorthIndian20ChandraKanada() -> Int { 184 | let h = helper([0, 3, 4, 7, 10, 11, 15]) 185 | return tuningTable(fromFrequencies: h) 186 | } 187 | 188 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 189 | @discardableResult func presetPersian17NorthIndian21BasantMukhari() -> Int { 190 | let h = helper([0, 1, 5, 7, 10, 11, 14]) 191 | return tuningTable(fromFrequencies: h) 192 | } 193 | 194 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 195 | @discardableResult func presetPersian17NorthIndian22Champakali() -> Int { 196 | let h = helper([0, 3, 6, 8, 10, 13, 14]) 197 | return tuningTable(fromFrequencies: h) 198 | } 199 | 200 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 201 | @discardableResult func presetPersian17NorthIndian23Patdeep() -> Int { 202 | let h = helper([0, 3, 4, 7, 10, 13, 15]) 203 | return tuningTable(fromFrequencies: h) 204 | } 205 | 206 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 207 | @discardableResult func presetPersian17NorthIndian24MohanKauns() -> Int { 208 | let h = helper([0, 3, 5, 7, 10, 11, 14]) 209 | return tuningTable(fromFrequencies: h) 210 | } 211 | 212 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 213 | @discardableResult func presetPersian17NorthIndian25Parameswari() -> Int { 214 | let h = helper([0, 1, 4, 7, 10, 12, 14]) 215 | return tuningTable(fromFrequencies: h) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Sources/Microtonality/TuningTable+RecurrenceRelation.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ 2 | 3 | public extension TuningTable { 4 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 5 | @discardableResult func presetRecurrenceRelation01() -> Int { 6 | return tuningTable(fromFrequencies: [1, 34, 5, 21, 3, 13, 55]) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Microtonality/TuningTable+Scala.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ 2 | 3 | import Foundation 4 | 5 | public extension TuningTable { 6 | /// Use a Scala file to write the tuning table. Returns notes per octave or nil when file couldn't be read. 7 | func scalaFile(_ filePath: String) -> Int? { 8 | guard 9 | let contentData = FileManager.default.contents(atPath: filePath), 10 | let contentStr = String(data: contentData, encoding: .utf8) 11 | else { 12 | print("can't read filePath: \(filePath)") 13 | return nil 14 | } 15 | 16 | let scalaFrequencies = frequencies(fromScalaString: contentStr) 17 | let npo = tuningTable(fromFrequencies: scalaFrequencies) 18 | return npo 19 | } 20 | 21 | fileprivate func stringTrimmedForLeadingAndTrailingWhiteSpacesFromString(_ inputString: String?) -> String? { 22 | guard let string = inputString else { 23 | return nil 24 | } 25 | 26 | let leadingTrailingWhiteSpacesPattern = "(?:^\\s+)|(?:\\s+$)" 27 | let regex: NSRegularExpression 28 | 29 | do { 30 | try regex = NSRegularExpression(pattern: leadingTrailingWhiteSpacesPattern, 31 | options: .caseInsensitive) 32 | } catch let error as NSError { 33 | print("ERROR: create regex: \(error)") 34 | return nil 35 | } 36 | 37 | let stringRange = NSRange(location: 0, length: string.count) 38 | let trimmedString = regex.stringByReplacingMatches(in: string, 39 | options: .reportProgress, 40 | range: stringRange, 41 | withTemplate: "$1") 42 | 43 | return trimmedString 44 | } 45 | 46 | /// Get frequencies from a Scala string 47 | func frequencies(fromScalaString rawStr: String?) -> [Frequency] { 48 | guard let inputStr = rawStr else { 49 | return [] 50 | } 51 | 52 | // default return value is [1.0] 53 | var scalaFrequencies = [Frequency(1)] 54 | var actualFrequencyCount = 1 55 | var frequencyCount = 1 56 | 57 | var parsedScala = true 58 | var parsedFirstCommentLine = false 59 | let values = inputStr.components(separatedBy: .newlines) 60 | var parsedFirstNonCommentLine = false 61 | var parsedAllFrequencies = false 62 | 63 | // REGEX match for a cents or ratio 64 | // (RATIO |CENTS ) 65 | // ( a / b |- a . b |- . b |- a .|- a ) 66 | let regexStr = "(\\d+\\/\\d+|-?\\d+\\.\\d+|-?\\.\\d+|-?\\d+\\.|-?\\d+)" 67 | let regex: NSRegularExpression 68 | do { 69 | regex = try NSRegularExpression(pattern: regexStr, 70 | options: .caseInsensitive) 71 | } catch let error as NSError { 72 | print("ERROR: cannot parse scala file: \(error)") 73 | return scalaFrequencies 74 | } 75 | 76 | for rawLineStr in values { 77 | var lineStr = stringTrimmedForLeadingAndTrailingWhiteSpacesFromString(rawLineStr) ?? rawLineStr 78 | 79 | if lineStr.isEmpty { continue } 80 | 81 | if lineStr.hasPrefix("!") { 82 | if !parsedFirstCommentLine { 83 | parsedFirstCommentLine = true 84 | #if false 85 | // currently not using the scala file name embedded in the file 86 | let components = lineStr.components(separatedBy: "!") 87 | if components.count > 1 { 88 | proposedScalaFilename = components[1] 89 | } 90 | #endif 91 | } 92 | continue 93 | } 94 | 95 | if !parsedFirstNonCommentLine { 96 | parsedFirstNonCommentLine = true 97 | #if false 98 | // currently not using the scala short description embedded in the file 99 | scalaShortDescription = lineStr 100 | #endif 101 | continue 102 | } 103 | 104 | if parsedFirstNonCommentLine, !parsedAllFrequencies { 105 | if let newFrequencyCount = Int(lineStr) { 106 | frequencyCount = newFrequencyCount 107 | if frequencyCount == 0 || frequencyCount > 127 { 108 | // #warning SPEC SAYS 0 notes is okay because 1/1 is implicit 109 | print("ERROR: number of notes in scala file: \(frequencyCount)") 110 | parsedScala = false 111 | break 112 | } else { 113 | parsedAllFrequencies = true 114 | continue 115 | } 116 | } 117 | } 118 | 119 | if actualFrequencyCount > frequencyCount { 120 | print("actual frequency cont: \(actualFrequencyCount) > frequency count: \(frequencyCount)") 121 | } 122 | 123 | /* The first note of 1/1 or 0.0 cents is implicit and not in the files.*/ 124 | 125 | // REGEX defined above this loop 126 | let rangeOfFirstMatch = regex.rangeOfFirstMatch( 127 | in: lineStr, 128 | options: .anchored, 129 | range: NSRange(location: 0, length: lineStr.count) 130 | ) 131 | 132 | if NSEqualRanges(rangeOfFirstMatch, NSRange(location: NSNotFound, length: 0)) == false { 133 | let nsLineStr = lineStr as NSString? 134 | let substringForFirstMatch = nsLineStr?.substring(with: rangeOfFirstMatch) as NSString? ?? "" 135 | if substringForFirstMatch.range(of: ".").length != 0 { 136 | if var scaleDegree = Frequency(lineStr) { 137 | // ignore 0.0...same as 1.0, 2.0, etc. 138 | if scaleDegree != 0 { 139 | scaleDegree = fabs(scaleDegree) 140 | // convert from cents to frequency 141 | scaleDegree /= 1200 142 | scaleDegree = pow(2, scaleDegree) 143 | scalaFrequencies.append(scaleDegree) 144 | actualFrequencyCount += 1 145 | continue 146 | } 147 | } 148 | } else { 149 | if substringForFirstMatch.range(of: "/").length != 0 { 150 | if substringForFirstMatch.range(of: "-").length != 0 { 151 | print("ERROR: invalid ratio: \(substringForFirstMatch)") 152 | parsedScala = false 153 | break 154 | } 155 | // Parse rational numerator/denominator 156 | let slashPos = substringForFirstMatch.range(of: "/") 157 | let numeratorStr = substringForFirstMatch.substring(to: slashPos.location) 158 | let numerator = Int(numeratorStr) ?? 0 159 | let denominatorStr = substringForFirstMatch.substring(from: slashPos.location + 1) 160 | let denominator = Int(denominatorStr) ?? 0 161 | if denominator == 0 { 162 | print("ERROR: invalid ratio: \(substringForFirstMatch)") 163 | parsedScala = false 164 | break 165 | } else { 166 | let mt = Frequency(numerator) / Frequency(denominator) 167 | if mt == 1.0 || mt == 2.0 { 168 | // skip 1/1, 2/1 169 | continue 170 | } else { 171 | scalaFrequencies.append(mt) 172 | actualFrequencyCount += 1 173 | continue 174 | } 175 | } 176 | } else { 177 | // a whole number, treated as a rational with a denominator of 1 178 | if let whole = Int(substringForFirstMatch as String) { 179 | if whole <= 0 { 180 | print("ERROR: invalid ratio: \(substringForFirstMatch)") 181 | parsedScala = false 182 | break 183 | } else if whole == 1 || whole == 2 { 184 | // skip degrees of 1 or 2 185 | continue 186 | } else { 187 | scalaFrequencies.append(Frequency(whole)) 188 | actualFrequencyCount += 1 189 | continue 190 | } 191 | } 192 | } 193 | } 194 | } else { 195 | print("ERROR: error parsing: \(lineStr)") 196 | continue 197 | } 198 | } 199 | 200 | if !parsedScala { 201 | print("FATAL ERROR: cannot parse Scala file") 202 | return [] 203 | } 204 | 205 | print("frequencies: \(scalaFrequencies)") 206 | return scalaFrequencies 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Sources/Microtonality/TuningTable+Wilson.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ 2 | 3 | public extension TuningTable { 4 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 5 | func presetHighlandBagPipes() -> Int { 6 | let npo = tuningTable(fromFrequencies: [32, 36, 39, 171, 48, 52, 57]) 7 | return npo 8 | } 9 | 10 | /// From Erv Wilson. See http://anaphoria.com/genus.pdf 11 | func presetDiaphonicTetrachord() -> Int { 12 | let npo = tuningTable(fromFrequencies: [1, 27 / 26.0, 9 / 8.0, 4 / 3.0, 18 / 13.0, 3 / 2.0, 27 / 16.0]) 13 | return npo 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Microtonality/TuningTable.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ 2 | 3 | import Foundation 4 | 5 | public typealias MIDINoteNumber = UInt8 6 | 7 | // TODO: unit tests 8 | 9 | /// helper object to simulate a Swift tuple for ObjC interoperability 10 | public class TuningTableETNN: NSObject { 11 | /// MIDI Note Number 12 | public var nn: MIDINoteNumber = 60 13 | /// Pitch Bend 14 | public var pitchBend: Int = 16384 / 2 15 | /// Initial tuning table with note number and pitch Bend 16 | /// - Parameters: 17 | /// - nn: Note Number 18 | /// - pb: Pitch Bend 19 | public init(_ nn: MIDINoteNumber = 60, _ pb: Int = 16384 / 2) { 20 | self.nn = nn 21 | pitchBend = pb 22 | } 23 | } 24 | 25 | /// helper object to simulate a Swift tuple for ObjC interoperability 26 | public class TuningTableDelta12ET: NSObject { 27 | /// MIDI note number 28 | public var nn: MIDINoteNumber = 60 29 | 30 | /// Detuning in cents 31 | public var cents: Double = 0 32 | 33 | /// Initialize tuning table 34 | /// - Parameters: 35 | /// - nn: Note number 36 | /// - cents: Detuning cents 37 | public init(_ nn: MIDINoteNumber = 60, _ cents: Double = 0) { 38 | self.nn = nn 39 | self.cents = cents 40 | } 41 | } 42 | 43 | /// TuningTable provides high-level methods to create musically useful tuning tables 44 | public class TuningTable: TuningTableBase { 45 | /// an octave-based array of linear frequencies, processed to spread across all midi note numbers 46 | public private(set) var masterSet = [Frequency]() 47 | 48 | /// Note number for standard reference note 49 | public var middleCNoteNumber: MIDINoteNumber = 60 { 50 | didSet { 51 | updateTuningTableFromMasterSet() 52 | } 53 | } 54 | 55 | /// Frequency of standard reference note 56 | /// equivalent to noteToHz: return 440. * exp2((60 - 69)/12.) 57 | public var middleCFrequency: Frequency = 261.625_565_300_6 { 58 | didSet { 59 | updateTuningTableFromMasterSet() 60 | } 61 | } 62 | 63 | /// Octave number for standard reference note. Can be negative 64 | /// ..., -2, -1, 0, 1, 2, ... 65 | public var middleCOctave: Int = 0 { 66 | didSet { 67 | updateTuningTableFromMasterSet() 68 | } 69 | } 70 | 71 | /// Range of downwards Pitch Bend used in etNN calculation. Must match your synthesizer's pitch bend DOWN range 72 | /// etNNPitchBendRangeDown and etNNPitchBendRangeUp must cover a spread that is 73 | /// greater than the maximum distance between two notes in your octave. 74 | public var etNNPitchBendRangeDown: Cents = -50 { 75 | didSet { 76 | updateTuningTableFromMasterSet() 77 | } 78 | } 79 | 80 | internal let pitchBendLow: Double = 0 81 | 82 | /// Range of upwards Pitch Bend used in etNN calculation. Must match your synthesizer's pitch bend UP range 83 | /// etNNPitchBendRangeDown and etNNPitchBendRangeUp must cover a spread that is 84 | /// greater than the maximum distance between two notes in your octave. 85 | public var etNNPitchBendRangeUp: Cents = 50 { 86 | didSet { 87 | updateTuningTableFromMasterSet() 88 | } 89 | } 90 | 91 | internal let pitchBendHigh: Double = 16383 92 | 93 | internal var etNNDictionary = [MIDINoteNumber: TuningTableETNN]() 94 | 95 | /// Given the tuning table's MIDINoteNumber NN return an TuningTableETNN 96 | /// of the equivalent 12ET MIDINoteNumber plus Pitch Bend 97 | /// Returns nil if the tuning table's MIDINoteNumber cannot be mapped to 12ET 98 | /// - parameter nn: The tuning table's Note Number 99 | public func etNNPitchBend(NN nn: MIDINoteNumber) -> TuningTableETNN? { 100 | return etNNDictionary[nn] 101 | } 102 | 103 | internal var delta12ETDictionary = [MIDINoteNumber: TuningTableDelta12ET]() 104 | 105 | /// Given the tuning table's MIDINoteNumber NN return an 106 | /// TuningTableETNN of the equivalent 12ET MIDINoteNumber plus Pitch Bend 107 | /// Returns nil if the tuning table's MIDINoteNumber cannot be mapped to 12ET 108 | /// - parameter nn: The tuning table's Note Number 109 | public func delta12ET(NN nn: MIDINoteNumber) -> TuningTableDelta12ET? { 110 | return delta12ETDictionary[nn] 111 | } 112 | 113 | /// Notes Per Octave: The count of the masterSet array 114 | override public var npo: Int { 115 | return masterSet.count 116 | } 117 | 118 | /// Initialization for standard default 12 tone equal temperament 119 | override public init() { 120 | super.init() 121 | _ = defaultTuning() 122 | } 123 | 124 | /// Create the tuning using the input masterSet 125 | /// 126 | /// - parameter inputMasterSet: An array of frequencies, i.e., the "masterSet" 127 | /// 128 | @discardableResult public func tuningTable(fromFrequencies inputMasterSet: [Frequency]) -> Int { 129 | if inputMasterSet.isEmpty { 130 | print("No input frequencies") 131 | return 0 132 | } 133 | 134 | // octave reduce 135 | var frequenciesAreValid = true 136 | let frequenciesOctaveReduce = inputMasterSet.map { (frequency: Frequency) -> Frequency in 137 | if frequency == 0 { 138 | frequenciesAreValid = false 139 | return Frequency(1) 140 | } 141 | 142 | var l2 = abs(frequency) 143 | while l2 < 1 { 144 | l2 *= 2.0 145 | } 146 | while l2 >= 2 { 147 | l2 /= 2.0 148 | } 149 | 150 | return l2 151 | } 152 | 153 | if !frequenciesAreValid { 154 | print("Invalid input frequencies") 155 | return 0 156 | } 157 | 158 | // sort 159 | let frequenciesOctaveReducedSorted = frequenciesOctaveReduce.sorted { $0 < $1 } 160 | masterSet = frequenciesOctaveReducedSorted 161 | 162 | // update 163 | updateTuningTableFromMasterSet() 164 | 165 | return masterSet.count 166 | } 167 | 168 | /// Create the tuning based on deviations from 12ET by an array of cents 169 | /// 170 | /// - parameter centsArray: An array of 12 Cents. 171 | /// 12ET will be modified by the centsArray, including deviations which result in a root less than 1.0 172 | /// 173 | public func tuning12ETDeviation(centsArray: [Cents]) { 174 | // Cents array count must equal 12 175 | guard centsArray.count == 12 else { 176 | print("user error: centsArray must have 12 elements") 177 | return 178 | } 179 | 180 | // 12ET 181 | _ = twelveToneEqualTemperament() 182 | 183 | // This should never happen 184 | guard masterSet.count == 12 else { 185 | print("internal error: 12 et must have 12 tones") 186 | return 187 | } 188 | 189 | // Master Set is in Frequency space 190 | var masterSetProcessed = masterSet 191 | 192 | // Scale by cents => Frequency space 193 | for (index, cent) in centsArray.enumerated() { 194 | let centF = exp2(cent / 1200) 195 | masterSetProcessed[index] = masterSetProcessed[index] * centF 196 | } 197 | masterSet = masterSetProcessed 198 | 199 | // update 200 | updateTuningTableFromMasterSet() 201 | } 202 | 203 | // Assume masterSet is set and valid: Process and update tuning table. 204 | internal func updateTuningTableFromMasterSet() { 205 | etNNDictionary.removeAll(keepingCapacity: true) 206 | delta12ETDictionary.removeAll(keepingCapacity: true) 207 | 208 | for i in 0 ..< TuningTable.midiNoteCount { 209 | let ff = Frequency(i - Int(middleCNoteNumber)) / Frequency(masterSet.count) 210 | var ttOctaveFactor = Frequency(trunc(ff)) 211 | if ff < 0 { 212 | ttOctaveFactor -= 1 213 | } 214 | var frac = fabs(ttOctaveFactor - ff) 215 | if frac == 1 { 216 | frac = 0 217 | ttOctaveFactor += 1 218 | } 219 | let frequencyIndex = Int(round(frac * Frequency(masterSet.count))) 220 | let tone = Frequency(masterSet[frequencyIndex]) 221 | let lp2 = pow(2, ttOctaveFactor) 222 | 223 | var f = tone * lp2 * middleCFrequency 224 | if f < 0 { 225 | f = 0 226 | } else if f > TuningTable.NYQUIST { 227 | f = TuningTable.NYQUIST 228 | } 229 | tableData[i] = Frequency(f) 230 | 231 | // UPDATE etNNPitchBend 232 | if f <= 0 { continue } // defensive, in case clamp above is removed 233 | let freqAs12ETNN = Double(middleCNoteNumber) + 12 * log2(f / middleCFrequency) 234 | if freqAs12ETNN >= 0, freqAs12ETNN < Double(TuningTable.midiNoteCount) { 235 | let etnnt = modf(freqAs12ETNN) 236 | var nnAs12ETNN = MIDINoteNumber(etnnt.0) // integer part "12ET note number" 237 | var etnnpbf = 100 * etnnt.1 // convert fractional part to Cents 238 | 239 | // if fractional part is [0.5,1.0] then flip it: add one to note number and negate pitchbend. 240 | if etnnpbf >= 50, nnAs12ETNN < MIDINoteNumber(TuningTable.midiNoteCount - 1) { 241 | nnAs12ETNN += 1 242 | etnnpbf -= 100 243 | } 244 | let delta12ETpbf = etnnpbf // defensive, in case you further modify etnnpbf 245 | let netnnpbf = etnnpbf / (etNNPitchBendRangeUp - etNNPitchBendRangeDown) 246 | if netnnpbf >= -0.5, netnnpbf <= 0.5 { 247 | let netnnpb = Int((netnnpbf + 0.5) * (pitchBendHigh - pitchBendLow) + pitchBendLow) 248 | etNNDictionary[MIDINoteNumber(i)] = TuningTableETNN(nnAs12ETNN, netnnpb) 249 | delta12ETDictionary[MIDINoteNumber(i)] = TuningTableDelta12ET(nnAs12ETNN, delta12ETpbf) 250 | } 251 | } 252 | } 253 | // print("etnn dictionary:\(etNNDictionary)") 254 | } 255 | 256 | /// Renders and returns the masterSet values as an array of cents 257 | public func masterSetInCents() -> [Cents] { 258 | let cents = masterSet.map { log2($0) * 1200 } 259 | return cents 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /Sources/Microtonality/TuningTableBase.swift: -------------------------------------------------------------------------------- 1 | // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ 2 | 3 | import Foundation 4 | 5 | /// TuningTableBase provides low-level methods for creating 6 | /// arbitrary mappings of midi note numbers to musical frequencies 7 | /// The default behavior is "12-tone equal temperament" so 8 | /// we can integrate in non-microtonal settings with backwards compatibility 9 | open class TuningTableBase: NSObject { 10 | // Definitions: 11 | // f = Frequency 12 | // p = Pitch = log2(frequency) for tunings where octave powers of 2 13 | // c = Cents = 1200 * Pitch 14 | // nn = midi note number of any tuning. maps to frequency in this tuning table. 15 | 16 | // Regarding MIDI/Pitchbend ("etNNPitchBend") scheme: 17 | // etnn or 12ETNN = midi note number of 12ET. 1 12ETNN = 1 semitone = 100 cents 18 | // More tolerance for numerical precision means less voice-stealing will happen with midi/pitchbend schemes 19 | 20 | /// For clarity, typealias Frequency as a Double 21 | public typealias Frequency = Double 22 | 23 | /// For clarify, typealias Cents as a Double. 24 | /// Cents = 1200 * log2(Frequency) 25 | public typealias Cents = Double 26 | 27 | /// Standard Nyquist frequency 28 | public static let NYQUIST: Frequency = 22050 // sampleRate / 2 29 | 30 | /// Total number of MIDI Notes available to play 31 | public static let midiNoteCount = 128 32 | 33 | internal var tableData = [Frequency](repeating: 1.0, count: midiNoteCount) 34 | 35 | /// Initialization for standard default 12 tone equal temperament 36 | override public init() { 37 | super.init() 38 | for noteNumber in 0 ..< TuningTable.midiNoteCount { 39 | let f = 440 * exp2(Double(noteNumber - 69) / 12.0) 40 | setFrequency(f, at: MIDINoteNumber(noteNumber)) 41 | } 42 | } 43 | 44 | /// Notes Per Octave: The count of the frequency array 45 | /// Defaults to 12 for the base class...should be overridden by subclasses 46 | public var npo: Int { 47 | return 12 48 | } 49 | 50 | /// Return the Frequency for the given MIDINoteNumber 51 | public func frequency(forNoteNumber noteNumber: MIDINoteNumber) -> Frequency { 52 | return tableData[Int(noteNumber)] 53 | } 54 | 55 | /// Set frequency of a given note number 56 | public func setFrequency(_ frequency: Frequency, at noteNumber: MIDINoteNumber) { 57 | tableData[Int(noteNumber)] = frequency 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/MicrotonalityTests/MicrotonalityTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Microtonality 2 | import XCTest 3 | 4 | final class MicrotonalityTests: XCTestCase { 5 | func testExample() {} 6 | } 7 | -------------------------------------------------------------------------------- /images/synthone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AudioKit/Microtonality/9ea80b46d2cdbc65c8d47b4422657d73ba161194/images/synthone.jpg --------------------------------------------------------------------------------