【问题标题】:Is it feasable to use AVAudioEngine to detect pitch in real time?使用 AVAudioEngine 实时检测音高是否可行?
【发布时间】:2016-06-12 04:16:51
【问题描述】:

我正在尝试编写一个以检测音高为核心的音乐应用程序。我已经看到了这个问题的解决方案以及 AppStore 上的应用程序。然而,它们中的大多数都已经过时了,我想做的是 Swift。我一直在寻找 AVAudioEngine 作为一种方法来做到这一点,但我发现缺少文档,或者我可能还不够努力。

我发现我可以像这样点击 inputNode 总线:

self.audioEngine = AVAudioEngine()
self.audioInputNode = self.audioEngine.inputNode!
self.audioInputNode.installTapOnBus(0, bufferSize:256, format: audioInputNode.outputFormatForBus(0), block: {(buffer, time) in
      self.analyzeBuffer(buffer)
})

总线每秒被点击 2-3 次,每次点击缓冲区包含超过 16000 个浮点数。这些幅度样本来自麦克风吗?

文档至少声称它是从节点输出的:“buffer 参数是从 AVAudioNode 的输出中捕获的音频缓冲区。

是否可以使用 AVAudioEngine 实时检测音高,还是我应该用另一种方式进行?

【问题讨论】:

    标签: ios swift avaudioengine pitch-detection


    【解决方案1】:

    这里有几个不同的概念。 AVAudioEngine 只是获取原始 PCM 数据的引擎,您可以直接使用 Novocaine、Core-Audio 或其他选项。

    PCM 数据是来自麦克风的浮点样本。

    就音高跟踪而言,有多种技术。需要注意的一点是,频率检测不同于音高检测。

    FFT 这很好,但无法检测缺少基本信号的信号音高。您需要将信号通过低通滤波器以减少可能高于Nyquist Frequency 的频率混叠,然后在将其传递给 FFT 之前将其window,这是为了减少spectral leakage。 FFT 会输出一系列 bin 内的频谱内容,值最高的 bin 被称为信号中最强的频率。

    Autocorrelation 可以提供更好的结果。它基本上是与自身相关的信号。

    最终取决于您想要检测的内容,有一些注意事项需要考虑。通过在未经预处理的缓冲区上运行正常的 FFT,诸如男声和某些乐器之类的东西可能会给出不正确的结果。

    检查这个PITCH DETECTION METHODS REVIEW

    就 Swift 而言,它不太适合实时、注重性能的系统。您可以查看old benchmarks of Swift vs C++

    C++ FFT 实现速度提高了 24 倍以上

    【讨论】:

    • 这些基准测试已有一年多的历史了。从那时起,斯威夫特已经走了很长一段路,所以我会对他们持怀疑态度。
    • 很公平,我的观点仍然是关于性能,Swift 不能替代 C++/C 来处理音频
    • 感谢您提供这篇内容丰富的帖子。我测试了上面的代码,假设缓冲区包含自上次点击以来的持续时间的采样幅度。我使用 Accelerate.framework 通过 FFT 传递缓冲区并取结果的平均值。使用音调发生器创建 A4 我测试了结果,它确实显示了预期的 440 Hz +- e。这意味着这是可能的。但是我想知道是否有更好的方法来做到这一点。我还没有检查 AudioKit,也没有探索错误的含义。例如。如果我将麦克风靠近扬声器,它只会显示 440 Hz。
    • 当然,FFT 会识别出清晰的音调。试图从麦克风检测音高,例如歌手或歌曲,可能不太准确。
    • 我只需要识别清晰的音调,也许还有三和弦。但是当麦克风离声源稍远时(例如吉他钢琴),我仍然需要找出一种方法来识别音调。有没有办法标准化振幅?
    【解决方案2】:

    我意识到 Hellium3 确实为我提供了关于音高是什么以及使用 Swift 做这些事情是否是个好主意的信息。

    我的问题最初是关于敲击 PCM 总线是否是从麦克风获取输入信号的方式。

    自从提出这个问题后,我就做到了。使用通过点击 PCM 总线获得的数据并分析缓冲区窗口。

    它工作得非常好,正是我对什么是 PCM 总线、缓冲区和采样频率缺乏了解,这让我首先提出了这个问题。

    了解这三个可以更容易地看出这是正确的。

    编辑:根据需要,我将粘贴我的(已弃用的)PitchDetector 实现。

    class PitchDetector {
      var samplingFrequency: Float
      var harmonicConstant: Float
    
      init(harmonicConstant: Float, samplingFrequency: Float) {
        self.harmonicConstant = harmonicConstant
        self.samplingFrequency = samplingFrequency
      }
    
      //------------------------------------------------------------------------------
      // MARK: Signal processing
      //------------------------------------------------------------------------------
    
      func detectPitch(_ samples: [Float]) -> Pitch? {
        let snac = self.snac(samples)
        let (lags, peaks) = self.findKeyMaxima(snac)
        let (τBest, clarity) = self.findBestPeak(lags, peaks: peaks)
        if τBest > 0 {
          let frequency = self.samplingFrequency / τBest
          if PitchManager.sharedManager.inManageableRange(frequency) {
            return Pitch(measuredFrequency: frequency, clarity: clarity)
          }
        }
    
        return nil
      }
    
      // Returns a Special Normalision of the AutoCorrelation function array for various lags with values between -1 and 1
      private func snac(_ samples: [Float]) -> [Float] {
        let τMax = Int(self.samplingFrequency / PitchManager.sharedManager.noteFrequencies.first!) + 1
        var snac = [Float](repeating: 0.0, count: samples.count)
        let acf = self.acf(samples)
        let norm = self.m(samples)
        for τ in 1 ..< τMax {
          snac[τ] = 2 * acf[τ + acf.count / 2] / norm[τ]
        }
    
        return snac
      }
    
      // Auto correlation function
      private func acf(_ x: [Float]) -> [Float] {
        let resultSize = 2 * x.count - 1
        var result = [Float](repeating: 0, count: resultSize)
        let xPad = repeatElement(Float(0.0), count: x.count - 1)
        let xPadded = xPad + x + xPad
        vDSP_conv(xPadded, 1, x, 1, &result, 1, vDSP_Length(resultSize), vDSP_Length(x.count))
    
        return result
      }
    
      private func m(_ samples: [Float]) -> [Float] {
        var sum: Float = 0.0
        for i in 0 ..< samples.count {
          sum += 2.0 * samples[i] * samples[i]
        }
        var m = [Float](repeating: 0.0, count: samples.count)
        m[0] = sum
        for i in 1 ..< samples.count {
          m[i] = m[i - 1] - samples[i - 1] * samples[i - 1] - samples[samples.count - i - 1] * samples[samples.count - i - 1]
        }
        return m
      }
    
      /**
       * Finds the indices of all key maximum points in data
       */
      private func findKeyMaxima(_ data: [Float]) -> (lags: [Float], peaks: [Float]) {
        var keyMaximaLags: [Float] = []
        var keyMaximaPeaks: [Float] = []
        var newPeakIncoming = false
        var currentBestPeak: Float = 0.0
        var currentBestτ = -1
        for τ in 0 ..< data.count {
          newPeakIncoming = newPeakIncoming || ((data[τ] < 0) && (data[τ + 1] > 0))
          if newPeakIncoming {
            if data[τ] > currentBestPeak {
              currentBestPeak = data[τ]
              currentBestτ = τ
            }
            let zeroCrossing = (data[τ] > 0) && (data[τ + 1] < 0)
            if zeroCrossing {
              let (τEst, peakEst) = self.approximateTruePeak(currentBestτ, data: data)
              keyMaximaLags.append(τEst)
              keyMaximaPeaks.append(peakEst)
              newPeakIncoming = false
              currentBestPeak = 0.0
              currentBestτ = -1
            }
          }
        }
    
        if keyMaximaLags.count <= 1 {
          let unwantedPeakOfLowPitchTone = (keyMaximaLags.count == 1 && data[Int(keyMaximaLags[0])] < data.max()!)
          if unwantedPeakOfLowPitchTone {
            keyMaximaLags.removeAll()
            keyMaximaPeaks.removeAll()
          }
          let (τEst, peakEst) = self.approximateTruePeak(data.index(of: data.max()!)!, data: data)
          keyMaximaLags.append(τEst)
          keyMaximaPeaks.append(peakEst)
        }
    
        return (lags: keyMaximaLags, peaks: keyMaximaPeaks)
      }
    
      /**
       * Approximates the true peak according to https://www.dsprelated.com/freebooks/sasp/Quadratic_Interpolation_Spectral_Peaks.html
       */
      private func approximateTruePeak(_ τ: Int, data: [Float]) -> (τEst: Float, peakEst: Float) {
        let α = data[τ - 1]
        let β = data[τ]
        let γ = data[τ + 1]
        let p = 0.5 * ((α - γ) / (α - 2.0 * β + γ))
        let peakEst = min(1.0, β - 0.25 * (α - γ) * p)
        let τEst = Float(τ) + p
    
        return (τEst, peakEst)
      }
    
      private func findBestPeak(_ lags: [Float], peaks: [Float]) -> (τBest: Float, clarity: Float) {
        let threshold: Float = self.harmonicConstant * peaks.max()!
        for i in 0 ..< peaks.count {
          if peaks[i] > threshold {
            return (τBest: lags[i], clarity: peaks[i])
          }
        }
    
        return (τBest: lags[0], clarity: peaks[0])
      }
    }
    

    感谢 Philip McLeod,他的研究成果用于我的上述实现。 http://www.cs.otago.ac.nz/research/publications/oucs-2008-03.pdf

    【讨论】:

    • 你用 Swift 实现了吗?如果可行,您能否分享为音高检测或示例项目创建的文件?我正在使用 AVAudioRecorder 录制音频
    • 你有机会分享你的实现吗?
    • 是的,如果你真的想要实现的话。但我后来用 AudioKit 代替了它。它工作得一样好(可能更好)并得到维护。 audiokit.io
    • @iOSGeek 我已经在上面添加了我的旧 PitchDetector
    • @jjatie 见上面的实现
    猜你喜欢
    • 2015-11-24
    • 2010-11-30
    • 2010-11-24
    • 2014-08-14
    • 2022-01-20
    • 1970-01-01
    • 2011-05-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多