// // Tuner.swift // KulexiuForStudent // // Created by 王智 on 2022/10/13. // import Foundation import AudioKit import AudioKitEX import SoundpipeAudioKit private let flats = ["C", "D♭","D","E♭","E","F","G♭","G","A♭","A","B♭","B"] private let sharps = ["C", "C♯","D","D♯","E","F","F♯","G","G♯","A","A♯","B"] private let frequencies: [Float] = [ 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 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 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 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 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 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 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, // 6 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951, // 7 4186, 4435, 4699, 4978, 5274, 5588, 5920, 6272, 6645, 7040, 7459, 7902 // 8 ] /** Types adopting the TunerDelegate protocol act as callbacks for Tuners and are the mechanism by which you may receive and respond to new information decoded by a Tuner. */ @objc public protocol TunerDelegate { /** Called by a Tuner on each update. - parameter tuner: Tuner that performed the update. - parameter output: Contains information decoded by the Tuner. */ func tunerDidUpdate(_ tuner: Tuner, output: TunerOutput) } // MARK:- TunerOutput /** Contains information decoded by a Tuner, such as frequency, octave, pitch, etc. */ @objc public class TunerOutput: NSObject { /** The octave of the interpreted pitch. */ @objc public fileprivate(set) var octave: Int = 0 /** The interpreted pitch of the microphone audio. */ @objc public fileprivate(set) var pitch: String = "" /** The octave of the interpreted pre pitch. */ @objc public fileprivate(set) var preOctave: Int = 0 /** The interpreted pre pitch of the microphone audio. */ @objc public fileprivate(set) var prePitch: String = "" /** The octave of the interpreted next pitch. */ @objc public fileprivate(set) var nextOctave: Int = 0 /** The interpreted next pitch of the microphone audio. */ @objc public fileprivate(set) var nextPitch: String = "" /** The octave of the interpreted transfer pitch. */ @objc public fileprivate(set) var transferOctave: Int = 0 /** The interpreted transfer pitch of the microphone audio. */ @objc public fileprivate(set) var transferPitch: String = "" /** The difference between the frequency of the interpreted pitch and the actual frequency of the microphone audio. For example if the microphone audio has a frequency of 432Hz, the pitch will be interpreted as A4 (440Hz), thus making the distance -8Hz. */ @objc public fileprivate(set) var distance: Float = 0.0 /** The amplitude of the microphone audio. */ @objc public fileprivate(set) var amplitude: Float = 0.0 /** The frequency of the microphone audio. */ @objc public fileprivate(set) var frequency: Float = 0.0 fileprivate override init() {} } /** A Tuner uses the devices microphone and interprets the frequency, pitch, etc. */ @objc public class Tuner: NSObject { fileprivate let smoothingBufferCount = 30 // 设置的A4标准频率 @objc public var A4Frequence: Int // 移调 @objc public var toneChangeRate: Int fileprivate let threshold: Float fileprivate let smoothing: Float fileprivate var engine: AudioEngine? fileprivate var microphone: AudioEngine.InputNode? fileprivate var pitchTap: PitchTap? fileprivate var silence: Fader? fileprivate var smoothingBuffer: [Float] = [] /** Object adopting the TunerDelegate protocol that should receive callbacks from this tuner. */ @objc public var delegate: TunerDelegate? /** Initializes a new Tuner. - parameter threshold: The minimum amplitude to recognize, 0 < threshold < 1 - parameter smoothing: Exponential smoothing factor, 0 < smoothing < 1 */ @objc public init(threshold: Float = 0.0, smoothing: Float = 0.25) { self.threshold = Float(min(abs(threshold), 1.0)) self.smoothing = Float(min(abs(smoothing), 1.0)) self.A4Frequence = 440 self.toneChangeRate = 0; } /** Starts the tuner. */ @objc public func start() { engine = AudioEngine() microphone = engine!.input silence = Fader(microphone!, gain: 0) /** 调大bufferSize ,可以降低采样频率 16384 (10次/秒) 8192 (20次/秒) 4096 (40次/秒) def */ pitchTap = PitchTap(microphone!, bufferSize: 4096, handler: tap_handler) microphone!.start() pitchTap!.start() engine!.output = silence try? engine!.start() } /** Stops the tuner. */ @objc public func stop() { microphone!.stop() pitchTap!.stop() engine!.stop() } func tap_handler(freq: [Float], amp: [Float]) -> Void { #if DEBUG print("freq -- real_\(freq[0]) -- imag_\(freq[1])" ) print("amp -- real_\(amp[0]) -- imag_\(amp[1])") #endif if let d = self.delegate { if amp[0] > self.threshold { let amplitude = amp[0] let frequency = freq[0] let output = Tuner.newOutput(frequency, amplitude) DispatchQueue.main.async { d.tunerDidUpdate(self, output: output) } } } } /** Exponential smoothing: 指数平滑算法 smoothing 平滑常数 https://en.wikipedia.org/wiki/Exponential_smoothing */ fileprivate func smooth(_ value: Float) -> Float { var frequency = value if smoothingBuffer.count > 0 { let last = smoothingBuffer.last! frequency = (smoothing * value) + (1.0 - smoothing) * last if smoothingBuffer.count > smoothingBufferCount { smoothingBuffer.removeFirst() } } smoothingBuffer.append(frequency) return frequency } /** Transfer frequency and amplitude to TunerOutput */ static func newOutput(_ frequency: Float, _ amplitude: Float) -> TunerOutput { let output = TunerOutput() var norm = frequency while norm > frequencies[frequencies.count - 1] { norm = norm / 2.0 } while norm < frequencies[0] { norm = norm * 2.0 } var i = -1 var min = Float.infinity for n in 0...frequencies.count-1 { let diff = frequencies[n] - norm if abs(diff) < abs(min) { min = diff i = n } } output.octave = i / 12 output.frequency = frequency output.amplitude = amplitude output.distance = frequency - frequencies[i] output.pitch = String(format: "%@", flats[i % flats.count], sharps[i % sharps.count]) var index = i-1 if i == 0 { index = 0 } else if i+1 == frequencies.count-1 { index = frequencies.count-2 } output.preOctave = (index) / 12 output.prePitch = String(format: "%@", flats[(index) % flats.count], sharps[(index) % sharps.count]) output.nextOctave = (index+2) / 12 output.nextPitch = String(format: "%@", flats[(index+2) % flats.count], sharps[(index+2) % sharps.count]) return output } @objc public func getTransferPitch(A4Frequency: Float, toneChangeRate: Int, output: TunerOutput)->TunerOutput { let newOutput = TunerOutput() var norm = output.frequency while norm > frequencies[frequencies.count - 1] { norm = norm / 2.0 } while norm < frequencies[0] { norm = norm * 2.0 } var i = -1 var min = Float.infinity for n in 0...frequencies.count-1 { let diff = frequencies[n] - norm if abs(diff) < abs(min) { min = diff i = n } } var index = i-1+toneChangeRate if index <= 0 { index = 0 } else if index+2 >= frequencies.count-1 { index = frequencies.count-2 } newOutput.octave = output.octave newOutput.frequency = output.frequency newOutput.amplitude = output.amplitude newOutput.distance = output.frequency / A4Frequency * 440 - frequencies[i] // 将收到的频率转成 440hz频率后对比 newOutput.pitch = output.pitch newOutput.preOctave = (index) / 12 newOutput.prePitch = String(format: "%@", flats[(index) % flats.count],sharps[(index) % sharps.count]) newOutput.nextOctave = (index+2) / 12 newOutput.nextPitch = String(format: "%@", flats[(index+2) % flats.count],sharps[(index+2) % sharps.count]) // 移调之后的显示 newOutput.transferOctave = (index+1) / 12 newOutput.transferPitch = String(format: "%@", flats[(index+1) % flats.count],sharps[(index+1) % sharps.count]) return newOutput } }