├── .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 | [](https://github.com/AudioKit/Microtonality/actions?query=workflow%3ACI)
6 | [](https://github.com/AudioKit/Microtonality/blob/main/LICENSE)
7 | [](https://houndci.com)
8 | [](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
--------------------------------------------------------------------------------