Возможно ли использовать AVAudioEngine для определения высоты звука в реальном времени?
Я пытаюсь написать музыкальное приложение, в котором определение высоты звука является ядром всего этого. Я видел решения этой проблемы, а также приложения в 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 операций с плавающей запятой для каждого касания. Это амплитудные отсчеты с микрофона?
Документы, по крайней мере, утверждают, что это вывод от узла: "Параметр буфера - это буфер аудио, захваченный с выхода AVAudioNode".
Можно ли использовать AVAudioEngine для определения высоты звука в реальном времени или я должен пойти по этому пути другим путем?
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])
}
}
Вся благодарность Филиппу Маклеоду, чьи исследования используются в моей реализации выше. http://www.cs.otago.ac.nz/research/publications/oucs-2008-03.pdf
Несколько разных концепций здесь. AVAudioEngine - это просто механизм, который получает необработанные данные PCM, вы можете использовать Novocaine, Core-Audio напрямую или другие варианты.
Данные PCM - это сэмплы с плавающей запятой с микрофона.
Что касается отслеживания высоты тона, существуют различные методы. Стоит отметить, что определение частоты отличается от определения высоты тона.
БПФ, который хорош, но не сможет обнаружить высоту сигнала с отсутствующими основами. Вам нужно будет пропустить сигнал через фильтр нижних частот, чтобы уменьшить возможное сглаживание частот выше частоты Найквиста, а затем отобразить его перед передачей в FFT, чтобы уменьшить спектральную утечку. БПФ будет выводить спектральное содержимое внутри серии элементов разрешения, причем элемент с самым высоким значением считается самой сильной частотой в сигнале.
Автокорреляция, которая может дать лучшие результаты. Это в основном сигнал, связанный с самим собой.
В конце концов, все сводится к тому, что вы хотели бы обнаружить. Такие вещи, как мужской голос и некоторые инструменты, могут давать неправильные результаты с помощью обычного БПФ, работающего с буферами, которые не были предварительно обработаны.
Проверьте этот ОБЗОР МЕТОДОВ ОБНАРУЖЕНИЯ ПИТЧА
Что касается Swift, то он не очень подходит для систем реального времени, ориентированных на производительность. Вы можете проверить старые тесты Swift vs C++
реализация C++ FFT более чем в 24 раза быстрее