我正在使用Apple的Accelerate框架在iPhone上实现FFT音高检测,如前所述[{3}} many。
我理解相位偏移,bin频率,并研究了几种使用FFT技术(简单音调检测,自相关,倒谱等)的开源调谐器来检测音调。这是我的问题:
我的FFT结果始终偏离5-10 Hz(+/-),即使这些分档仅相差1-2赫兹。我尝试了不同的算法,甚至简单以高分辨率采样的FFT显示出看似错误位置的幅度尖峰。这不是一致的偏移;有些太高,有些太低。
例如,440Hz的音调为445.2 Hz; 220Hz为214Hz; 880Hz为874Hz;使用音调发生器,1174Hz为1183Hz。对于使用几乎完全相同算法的Mac的类似times,完全检测音高没有问题。 (这些差异在设备上与模拟器不同,但它们仍处于关闭状态。)
我认为问题不是分辨率,因为在实际音调和检测到的幅度尖峰之间通常有几个分档。这就好像输入只是听错了。
我在下面粘贴了我的代码。一般流程很简单:
将步骤推到FFT缓冲器上 - > Hann Window - > FFT - >相位/幅度 - >最大音调错误。
enum {
kOversample = 4,
kSamples = MAX_FRAME_LENGTH,
kSamples2 = kSamples / 2,
kRange = kSamples * 5 / 16,
kStep = kSamples / kOversample
};
const int PENDING_LEN = kSamples * 5;
static float pendingAudio[PENDING_LEN * sizeof(float)];
static int pendingAudioLength = 0;
- (void)processBuffer {
static float window[kSamples];
static float phase[kRange];
static float lastPhase[kRange];
static float phaseDeltas[kRange];
static float frequencies[kRange];
static float slidingFFTBuffer[kSamples];
static float buffer[kSamples];
static BOOL initialized = NO;
if (!initialized) {
memset(lastPhase, 0, kRange * sizeof(float));
vDSP_hann_window(window, kSamples, 0);
initialized = YES;
}
BOOL canProcessNewStep = YES;
while (canProcessNewStep) {
@synchronized (self) {
if (pendingAudioLength < kStep) {
break; // not enough data
}
// Rotate one step's worth of pendingAudio onto the end of slidingFFTBuffer
memmove(slidingFFTBuffer, slidingFFTBuffer + kStep, (kSamples - kStep) * sizeof(float));
memmove(slidingFFTBuffer + (kSamples - kStep), pendingAudio, kStep * sizeof(float));
memmove(pendingAudio, pendingAudio + kStep, (PENDING_LEN - kStep) * sizeof(float));
pendingAudioLength -= kStep;
canProcessNewStep = (pendingAudioLength >= kStep);
}
// Hann Windowing
vDSP_vmul(slidingFFTBuffer, 1, window, 1, buffer, 1, kSamples);
vDSP_ctoz((COMPLEX *)buffer, 2, &splitComplex, 1, kSamples2);
// Carry out a Forward FFT transform.
vDSP_fft_zrip(fftSetup, &splitComplex, 1, log2f(kSamples), FFT_FORWARD);
// magnitude to decibels
static float magnitudes[kRange];
vDSP_zvmags(&splitComplex, 1, magnitudes, 1, kRange);
float zero = 1.0;
vDSP_vdbcon(magnitudes, 1, &zero, magnitudes, 1, kRange, 0); // to decibels
// phase
vDSP_zvphas(&splitComplex, 1, phase, 1, kRange); // compute magnitude and phase
vDSP_vsub(lastPhase, 1, phase, 1, phaseDeltas, 1, kRange); // compute phase difference
memcpy(lastPhase, phase, kRange * sizeof(float)); // save old phase
double freqPerBin = sampleRate / (double)kSamples;
double phaseStep = 2.0 * M_PI * (float)kStep / (float)kSamples;
// process phase difference ( via https://stackoverflow.com/questions/4633203 )
for (int k = 1; k < kRange; k++) {
double delta = phaseDeltas[k];
delta -= k * phaseStep; // subtract expected phase difference
delta = remainder(delta, 2.0 * M_PI); // map delta phase into +/- M_PI interval
delta /= phaseStep; // calculate diff from bin center frequency
frequencies[k] = (k + delta) * freqPerBin; // calculate the true frequency
}
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
MCTunerData *tunerData = [[[MCTunerData alloc] initWithSize:MAX_FRAME_LENGTH] autorelease];
double maxMag = -INFINITY;
float maxFreq = 0;
for (int i=0; i < kRange; i++) {
[tunerData addFrequency:frequencies[i] withMagnitude:magnitudes[i]];
if (magnitudes[i] > maxMag) {
maxFreq = frequencies[i];
maxMag = magnitudes[i];
}
}
NSLog(@"Max Frequency: %.1f", maxFreq);
[tunerData calculate];
// Update the UI with our newly acquired frequency value.
[self.delegate frequencyChangedWithValue:[tunerData mainFrequency] data:tunerData];
[pool drain];
}
}
OSStatus renderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames,
AudioBufferList *ioData)
{
MCTuner* tuner = (MCTuner *)inRefCon;
OSStatus err = AudioUnitRender(tuner->audioUnit, ioActionFlags, inTimeStamp, 1, inNumberFrames, tuner->bufferList);
if (err < 0) {
return err;
}
// convert SInt16 to float because iOS doesn't support recording floats directly
SInt16 *inputInts = (SInt16 *)tuner->bufferList->mBuffers[0].mData;
@synchronized (tuner) {
if (pendingAudioLength + inNumberFrames < PENDING_LEN) {
// Append the audio that just came in into the pending audio buffer, converting to float
// because iOS doesn't support recording floats directly
for(int i = 0; i < inNumberFrames; i++) {
pendingAudio[pendingAudioLength + i] = (inputInts[i] + 0.5) / 32767.5;
}
pendingAudioLength += inNumberFrames;
} else {
// the buffer got too far behind. Don't give any more audio data.
NSLog(@"Dropping frames...");
}
if (pendingAudioLength >= kStep) {
[tuner performSelectorOnMainThread:@selector(processBuffer) withObject:nil waitUntilDone:NO];
}
}
return noErr;
}
答案 0 :(得分:3)
我没有详细介绍你的代码,但是我突然想到了我:
vDSP_zvmags(&splitComplex, 1, magnitudes, 1, kRange);
重要的是要记住,真实到复杂的fft的结果是有点奇怪的布局。如果第j个傅立叶系数的实部和虚部由R(j)和I(j)表示,则real
对象的imag
和splitComplex
分量具有以下内容:
.real = { R(0) , R(1), R(2), ... , R(n/2 - 1) }
.imag = { R(n/2), I(1), I(2), ... , I(n/2 - 1) }
因此,你的量级计算有点奇怪;幅度向量中的第一个条目是sqrt(R(0)^2 + R(n/2)^2)
,它应该只是|R(0)|
。我没有仔细研究所有的常数,但似乎这导致了一个错误的错误,你失去了nyquist带(R(n/2)
)或类似的。这种类型的逐个误差可能导致频带被视为比实际更宽或更窄,这将导致在整个范围内放大或缩小的小音调,这匹配你看到了什么。
答案 1 :(得分:1)
我确信我的算法实际上并不是什么;相反,我使用Apple的AUGraph出了点问题。当我删除它只是使用普通音频单元而没有设置图表时,我能够让它正确识别音高。
答案 2 :(得分:1)
FFT是电锯,而不是手术刀。 一般来说,对于现实检查FFT编码,(1)使用Parseval定理进行测试(时域中的均方幅度应在舍入内等于频谱之和)和(2)逆FFT并且只是听取它。 对不起,但你似乎期待fft的绝对准确性太高了。你根本不会得到它。但是,您的代码中有一些要检查的小东西清单。大多数algos移动DC和Nyquist以使内存分配均匀,但您必须手动移动它所属的Nyquist术语并将各种事物归零:
A.realp[NOVER2] = A.imagp[0]; // move real Nyquist term to where it belongs
A.imagp[NOVER2] = 0.0; // this is zero
A.imagp[0] = 0.0; // make this a true zero
在音频数据上,DC应为零(例如,幅度为零均值),但在小窗口中,它可能不是。我一个人待着。 您正在做的事情远远超过您需要找到的最大bin(关于阶段声码器的评论是正确的)。使用hamm窗口的恕我直言会伤害准确性。我用 lot (4x)的零填充实际数据的末尾有更好的结果。祝你好运。
答案 3 :(得分:0)
您似乎不仅使用FFT,而且使用FFT后的相位声码器来调整估计的bin频率。根据相位增益和限制,相位声码器频率调整可以将估计的频率拉到FFT区间宽度之外。如果发生这种情况,使用较窄的箱(较长的FFT)将无济于事。您可能需要进行健全性检查,以确定是否要将峰值频率拉到其FFT频率区域之外。或者尝试取出相位声码器,看看FFT是否会返回更合理的结果。