The function currently works fine returning Double
and already gives the variable I need, namely frequency.
It is better not to change it. I will create a new class to add further functionality (i.e. mapping frequencies to keys of a MIDI keyboard). Thank you Rob and jtbandes for your input.
EDIT
It was more sensible to solve this without broadening the problem space. I found my solution once I questioned the need to return an argument to the function and asked instead how data could be presented from inside the function using only the available arguments. The solution addresses a related problem as identified by the accepted answer to another post and unwraps optional values without causing runtime errors as recommended on several other posts (here, here and here.)
Values are unwrapped using optional chaining and nil coalescing instead of forced unwrapping. Numbers that are valid according to these rules are converted to frequencies and mapped to a MIDI keyboard. Invalid tuning values and note strings marked with an ‘x’
(as specified by these rules) will generate a frequency of 0.0 Hz.
The solution allows Optional scale values that are read by the function to be compared more easily with frequencies returned as other functions are called.
e.g.
Octave map
0 Optional("x")
0 : 0.0
1 Optional("35/32")
1 : 286.1534375
2 Optional("x")
2 : 0.0
3 Optional("x")
3 : 0.0
4 Optional("5/4")
4 : 327.0325
5 Optional("21/16")
5 : 343.384125
6 Optional("x")
6 : 0.0
7 Optional("3/2")
7 : 392.439
8 Optional("x")
8 : 0.0
9 Optional("x")
9 : 0.0
10 Optional("7/4")
10 : 457.8455
11 Optional("15/8")
11 : 490.54875
It also helps with reading frequencies in other octaves
MIDI map
Octave 0
0 0 0.0
1 1 8.942294921875
2 2 0.0
3 3 0.0
4 4 10.219765625
5 5 10.73075390625
6 6 0.0
7 7 12.26371875
8 8 0.0
9 9 0.0
10 10 14.307671875
11 11 15.3296484375
Octave 1
12 0 0.0
13 1 17.88458984375
14 2 0.0
15 3 0.0
16 4 20.43953125
17 5 21.4615078125
18 6 0.0
19 7 24.5274375
20 8 0.0
21 9 0.0
22 10 28.61534375
23 11 30.659296875
Octave 2
24 0 0.0
25 1 35.7691796875
26 2 0.0
etc
Octave 9
108 0 0.0
109 1 4578.455
110 2 0.0
111 3 0.0
112 4 5232.52
113 5 5494.146
114 6 0.0
115 7 6279.024
116 8 0.0
117 9 0.0
118 10 7325.528
119 11 7848.78
Octave 10
120 0 0.0
121 1 9156.91
122 2 0.0
123 3 0.0
124 4 10465.04
125 5 10988.292
126 6 0.0
Developers of music apps may find this useful because it shows how to create a retuned MIDI map. The solution lets me unwrap numeric strings consisting of both fractions and decimals that specify the tuning of notes in musical scales that lie beyond the scope of a standard music keyboard. Its significance will not be lost on anyone who visits this site.
Here is the code
Tuner.swift
import UIKit
class Tuner {
var tuning = [String]() // .scl
var pitchClassFrequency = Double() // .scl
let centsPerOctave: Double = 1200.0 // .scl mandated by Scala tuning file format
let formalOctave: Double = 2.0 // .kbm/.scl Double for stretched-octave tunings
var octaveMap = [Double]() // .kbm/.scl
var midiMap = [Double]() // .kbm
let sizeOfMap = 12 // .kbm
let firstMIDIKey = 0 // .kbm
let lastMIDIKey = 127 // .kbm
let referenceMIDIKey = 60 // .kbm
let referenceFrequency: Double = 261.626 // .kbm frequency of middle C
var indexMIDIKeys = Int()
var indexOctaveKeys = Int()
var currentKeyOctave = Int()
var index: Int = 0
init(tuning: [String]) {
self.tuning = tuning
// SCL file format - create frequency map of notes for one octave
print("Octave map")
print("")
let _ = tuning.flatMap(scaleToFrequencies)
// KBM file format - create frequency map of MIDI keys 0-127
print("")
print("MIDI map")
let _ = createMIDIMap()
}
func createMIDIMap() {
indexOctaveKeys = firstMIDIKey // set indexOctaveKeys to pitchClass 0
currentKeyOctave = firstMIDIKey // set currentOctave to octave 0
for indexMIDIKeys in firstMIDIKey...lastMIDIKey {
let indexOctaveKeys = indexMIDIKeys % sizeOfMap
currentKeyOctave = Int((indexMIDIKeys) / sizeOfMap)
let frequency = octaveMap[indexOctaveKeys] * 2**Double(currentKeyOctave)
// midiMap[i] = octaveMap[indexMIDIKeys] * 2**Double(currentKeyOctave)
if indexOctaveKeys == 0 {
print("")
print("Octave \(currentKeyOctave)")
}
print(indexMIDIKeys, indexOctaveKeys, frequency)
}
}
func scaleToFrequencies(s: String?) {
var frequency: Double = 0.0
// first process non-numerics.
let numericString = zapAllButNumbersSlashDotAndX(s: s)
print(index, numericString as Any) // eavesdrop on String?
// then process numerics.
frequency = (processNumericsAndMap(numericString: numericString)) / Double(2)**Double(referenceMIDIKey / sizeOfMap)
octaveMap.append(frequency)
print(index,":",frequency * 2**Double(referenceMIDIKey / sizeOfMap))
index += 1
}
func processNumericsAndMap(numericString: String?) -> Double {
guard let slashToken = ((numericString?.contains("/")) ?? nil),
let dotToken = ((numericString?.contains(".")) ?? nil),
let xToken = ((numericString?.contains("x")) ?? nil),
slashToken == false,
dotToken == true,
xToken == false
else {
guard let dotToken = ((numericString?.contains(".")) ?? nil),
let xToken = ((numericString?.contains("x")) ?? nil),
dotToken == false,
xToken == false
else {
guard let xToken = ((numericString?.contains("x")) ?? nil),
xToken == false
else {
// then it must be mapping.
let frequency = 0.0
// print("[x] \(frequency) Hz")
return frequency
}
// then process integer.
let frequency = processInteger(s: numericString)
return frequency
}
// then process fractional.
let frequency = processFractional(s: numericString)
return frequency
}
// process decimal.
let frequency = processDecimal(s: numericString)
return frequency
}
func processFractional(s: String?) -> Double {
let parts = s?.components(separatedBy: "/")
guard parts?.count == 2,
let numerator = Double((parts?[0])?.digits ?? "failNumerator"),
let dividend = Double((parts?[1])?.digits ?? "failDenominator"),
dividend != 0
else {
let frequency = 0.0
print("invalid ratio: frequency now being set to \(frequency) Hz")
return frequency
}
let frequency = referenceFrequency * (numerator / dividend)
return frequency
}
func processDecimal(s: String?) -> Double {
let parts = s?.components(separatedBy: ".")
guard parts?.count == 2,
let intervalValue = Double(s ?? "failInterval"),
let _ = Double((parts?[0])?.digits ?? "failDecimal")
else {
let frequency = 0.0
print("invalid cents value: frequency now being forced to \(frequency) Hz ")
return frequency
}
let power = intervalValue/centsPerOctave // value with explicit remainder
let frequency = referenceFrequency * (formalOctave**power)
return frequency
}
func processInteger(s: String?) -> Double {
let frequency = 0.0
print("not cents, not ratio : frequency now being set to \(frequency) Hz ")
return frequency
}
func zapAllButNumbersSlashDotAndX(s: String?) -> String? {
var mixedString = s
if mixedString != nil {
mixedString = mixedString!
}
guard var _ = mixedString?.contains("/"),
var _ = mixedString?.contains(".")
else {
let numberToken = mixedString
return numberToken
}
guard let xToken = mixedString?.contains("x"),
xToken == false
else {
let xToken = "x"
return xToken
}
let notNumberCharacters = NSCharacterSet.decimalDigits.inverted
let numericString = s?.trimmingCharacters(in: notNumberCharacters) ?? "orElse"
return numericString.stringByRemovingWhitespaces
}
}
extension String {
var stringByRemovingWhitespaces: String {
return components(separatedBy: .whitespaces).joined(separator: "")
}
}
extension String {
var digits: String {
return components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
}
}
precedencegroup Exponentiative {
associativity: left
higherThan: MultiplicationPrecedence
}
infix operator ** : Exponentiative
func ** (num: Double, power: Double) -> Double {
return pow(num, power)
}
func pitchClass(pitchClass: Int, _ frequency: Double) -> Int {
return pitchClass
}
func frequency(pitchClass: Int, _ frequency: Double) -> Double {
return frequency
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
// Hexany: 6-note scale of Erv Wilson
let tuning = ["x", "35/32", "x", "x", "5/4", "21/16", "x", "3/2", "x", "x", "7/4", "15/8"]
// Diatonic scale: rational fractions
// let tuning = [ "1/1", "9/8", "5/4", "4/3", "3/2", "27/16", "15/8", "2/1"]
// Mohajira: rational fractions
// let tuning = [ "21/20", "9/8", "6/5", "49/40", "4/3", "7/5", "3/2", "8/5", "49/30", "9/5", "11/6", "2/1"]
// Diatonic scale: 12-tET
// let tuning = [ "0.0", "200.0", "400.0", "500", "700.0", "900.0", "1100.0", "1200.0"]
override func viewDidLoad() {
super.viewDidLoad()
_ = Tuner(tuning: tuning)
}
}