Tuner.swift 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. //
  2. // Tuner.swift
  3. // KulexiuForStudent
  4. //
  5. // Created by 王智 on 2022/10/13.
  6. //
  7. import Foundation
  8. import AudioKit
  9. import AudioKitEX
  10. import SoundpipeAudioKit
  11. private let flats = ["C", "D♭","D","E♭","E","F","G♭","G","A♭","A","B♭","B"]
  12. private let sharps = ["C", "C♯","D","D♯","E","F","F♯","G","G♯","A","A♯","B"]
  13. private let frequencies: [Float] = [
  14. 16.35, 17.32, 18.35, 19.45, 20.60, 21.83, 23.12, 24.50, 25.96, 27.50, 29.14, 30.87, // 0
  15. 32.70, 34.65, 36.71, 38.89, 41.20, 43.65, 46.25, 49.00, 51.91, 55.00, 58.27, 61.74, // 1
  16. 65.41, 69.30, 73.42, 77.78, 82.41, 87.31, 92.50, 98.00, 103.8, 110.0, 116.5, 123.5, // 2
  17. 130.8, 138.6, 146.8, 155.6, 164.8, 174.6, 185.0, 196.0, 207.7, 220.0, 233.1, 246.9, // 3
  18. 261.6, 277.2, 293.7, 311.1, 329.6, 349.2, 370.0, 392.0, 415.3, 440.0, 466.2, 493.9, // 4
  19. 523.3, 554.4, 587.3, 622.3, 659.3, 698.5, 740.0, 784.0, 830.6, 880.0, 932.3, 987.8, // 5
  20. 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, // 6
  21. 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951, // 7
  22. 4186, 4435, 4699, 4978, 5274, 5588, 5920, 6272, 6645, 7040, 7459, 7902 // 8
  23. ]
  24. /**
  25. Types adopting the TunerDelegate protocol act as callbacks for Tuners and are
  26. the mechanism by which you may receive and respond to new information decoded
  27. by a Tuner.
  28. */
  29. @objc public protocol TunerDelegate {
  30. /**
  31. Called by a Tuner on each update.
  32. - parameter tuner: Tuner that performed the update.
  33. - parameter output: Contains information decoded by the Tuner.
  34. */
  35. func tunerDidUpdate(_ tuner: Tuner, output: TunerOutput)
  36. }
  37. // MARK:- TunerOutput
  38. /**
  39. Contains information decoded by a Tuner, such as frequency, octave, pitch, etc.
  40. */
  41. @objc public class TunerOutput: NSObject {
  42. /**
  43. The octave of the interpreted pitch.
  44. */
  45. @objc public fileprivate(set) var octave: Int = 0
  46. /**
  47. The interpreted pitch of the microphone audio.
  48. */
  49. @objc public fileprivate(set) var pitch: String = ""
  50. /**
  51. The octave of the interpreted pre pitch.
  52. */
  53. @objc public fileprivate(set) var preOctave: Int = 0
  54. /**
  55. The interpreted pre pitch of the microphone audio.
  56. */
  57. @objc public fileprivate(set) var prePitch: String = ""
  58. /**
  59. The octave of the interpreted next pitch.
  60. */
  61. @objc public fileprivate(set) var nextOctave: Int = 0
  62. /**
  63. The interpreted next pitch of the microphone audio.
  64. */
  65. @objc public fileprivate(set) var nextPitch: String = ""
  66. /**
  67. The octave of the interpreted transfer pitch.
  68. */
  69. @objc public fileprivate(set) var transferOctave: Int = 0
  70. /**
  71. The interpreted transfer pitch of the microphone audio.
  72. */
  73. @objc public fileprivate(set) var transferPitch: String = ""
  74. /**
  75. The difference between the frequency of the interpreted pitch and the actual
  76. frequency of the microphone audio.
  77. For example if the microphone audio has a frequency of 432Hz, the pitch will
  78. be interpreted as A4 (440Hz), thus making the distance -8Hz.
  79. */
  80. @objc public fileprivate(set) var distance: Float = 0.0
  81. /**
  82. The amplitude of the microphone audio.
  83. */
  84. @objc public fileprivate(set) var amplitude: Float = 0.0
  85. /**
  86. The frequency of the microphone audio.
  87. */
  88. @objc public fileprivate(set) var frequency: Float = 0.0
  89. fileprivate override init() {}
  90. }
  91. /**
  92. A Tuner uses the devices microphone and interprets the frequency, pitch, etc.
  93. */
  94. @objc public class Tuner: NSObject {
  95. fileprivate let smoothingBufferCount = 30
  96. // 设置的A4标准频率
  97. @objc public var A4Frequence: Int
  98. // 移调
  99. @objc public var toneChangeRate: Int
  100. fileprivate let threshold: Float
  101. fileprivate let smoothing: Float
  102. fileprivate var engine: AudioEngine?
  103. fileprivate var microphone: AudioEngine.InputNode?
  104. fileprivate var pitchTap: PitchTap?
  105. fileprivate var silence: Fader?
  106. fileprivate var smoothingBuffer: [Float] = []
  107. /**
  108. Object adopting the TunerDelegate protocol that should receive callbacks
  109. from this tuner.
  110. */
  111. @objc public var delegate: TunerDelegate?
  112. /**
  113. Initializes a new Tuner.
  114. - parameter threshold: The minimum amplitude to recognize, 0 < threshold < 1
  115. - parameter smoothing: Exponential smoothing factor, 0 < smoothing < 1
  116. */
  117. @objc public init(threshold: Float = 0.0, smoothing: Float = 0.25) {
  118. self.threshold = Float(min(abs(threshold), 1.0))
  119. self.smoothing = Float(min(abs(smoothing), 1.0))
  120. self.A4Frequence = 440
  121. self.toneChangeRate = 0;
  122. }
  123. /**
  124. Starts the tuner.
  125. */
  126. @objc public func start() {
  127. engine = AudioEngine()
  128. microphone = engine!.input
  129. silence = Fader(microphone!, gain: 0)
  130. /**
  131. 调大bufferSize ,可以降低采样频率
  132. 16384 (10次/秒)
  133. 8192 (20次/秒)
  134. 4096 (40次/秒) def
  135. */
  136. pitchTap = PitchTap(microphone!, bufferSize: 4096, handler: tap_handler)
  137. microphone!.start()
  138. pitchTap!.start()
  139. engine!.output = silence
  140. try? engine!.start()
  141. }
  142. /**
  143. Stops the tuner.
  144. */
  145. @objc public func stop() {
  146. microphone!.stop()
  147. pitchTap!.stop()
  148. engine!.stop()
  149. }
  150. func tap_handler(freq: [Float], amp: [Float]) -> Void {
  151. #if DEBUG
  152. print("freq -- real_\(freq[0]) -- imag_\(freq[1])" )
  153. print("amp -- real_\(amp[0]) -- imag_\(amp[1])")
  154. #endif
  155. if let d = self.delegate {
  156. if amp[0] > self.threshold
  157. {
  158. let amplitude = amp[0]
  159. let frequency = freq[0]
  160. let output = Tuner.newOutput(frequency, amplitude)
  161. DispatchQueue.main.async {
  162. d.tunerDidUpdate(self, output: output)
  163. }
  164. }
  165. }
  166. }
  167. /**
  168. Exponential smoothing:
  169. 指数平滑算法
  170. smoothing 平滑常数
  171. https://en.wikipedia.org/wiki/Exponential_smoothing
  172. */
  173. fileprivate func smooth(_ value: Float) -> Float {
  174. var frequency = value
  175. if smoothingBuffer.count > 0 {
  176. let last = smoothingBuffer.last!
  177. frequency = (smoothing * value) + (1.0 - smoothing) * last
  178. if smoothingBuffer.count > smoothingBufferCount {
  179. smoothingBuffer.removeFirst()
  180. }
  181. }
  182. smoothingBuffer.append(frequency)
  183. return frequency
  184. }
  185. /**
  186. Transfer frequency and amplitude to TunerOutput
  187. */
  188. static func newOutput(_ frequency: Float, _ amplitude: Float) -> TunerOutput {
  189. let output = TunerOutput()
  190. var norm = frequency
  191. while norm > frequencies[frequencies.count - 1] {
  192. norm = norm / 2.0
  193. }
  194. while norm < frequencies[0] {
  195. norm = norm * 2.0
  196. }
  197. var i = -1
  198. var min = Float.infinity
  199. for n in 0...frequencies.count-1 {
  200. let diff = frequencies[n] - norm
  201. if abs(diff) < abs(min) {
  202. min = diff
  203. i = n
  204. }
  205. }
  206. output.octave = i / 12
  207. output.frequency = frequency
  208. output.amplitude = amplitude
  209. output.distance = frequency - frequencies[i]
  210. output.pitch = String(format: "%@", flats[i % flats.count], sharps[i % sharps.count])
  211. var index = i-1
  212. if i == 0 {
  213. index = 0
  214. } else if i+1 == frequencies.count-1 {
  215. index = frequencies.count-2
  216. }
  217. output.preOctave = (index) / 12
  218. output.prePitch = String(format: "%@", flats[(index) % flats.count], sharps[(index) % sharps.count])
  219. output.nextOctave = (index+2) / 12
  220. output.nextPitch = String(format: "%@", flats[(index+2) % flats.count], sharps[(index+2) % sharps.count])
  221. return output
  222. }
  223. @objc public func getTransferPitch(A4Frequency: Float, toneChangeRate: Int, output: TunerOutput)->TunerOutput {
  224. let newOutput = TunerOutput()
  225. var norm = output.frequency
  226. while norm > frequencies[frequencies.count - 1] {
  227. norm = norm / 2.0
  228. }
  229. while norm < frequencies[0] {
  230. norm = norm * 2.0
  231. }
  232. var i = -1
  233. var min = Float.infinity
  234. for n in 0...frequencies.count-1 {
  235. let diff = frequencies[n] - norm
  236. if abs(diff) < abs(min) {
  237. min = diff
  238. i = n
  239. }
  240. }
  241. var index = i-1+toneChangeRate
  242. if index <= 0 {
  243. index = 0
  244. } else if index+2 >= frequencies.count-1 {
  245. index = frequencies.count-2
  246. }
  247. newOutput.octave = output.octave
  248. newOutput.frequency = output.frequency
  249. newOutput.amplitude = output.amplitude
  250. newOutput.distance = output.frequency / A4Frequency * 440 - frequencies[i] // 将收到的频率转成 440hz频率后对比
  251. newOutput.pitch = output.pitch
  252. newOutput.preOctave = (index) / 12
  253. newOutput.prePitch = String(format: "%@", flats[(index) % flats.count],sharps[(index) % sharps.count])
  254. newOutput.nextOctave = (index+2) / 12
  255. newOutput.nextPitch = String(format: "%@", flats[(index+2) % flats.count],sharps[(index+2) % sharps.count])
  256. // 移调之后的显示
  257. newOutput.transferOctave = (index+1) / 12
  258. newOutput.transferPitch = String(format: "%@", flats[(index+1) % flats.count],sharps[(index+1) % sharps.count])
  259. return newOutput
  260. }
  261. }