核心音频:Float32 到 SInt16 的转换伪像

Core Audio: Float32 to SInt16 conversion artefacts

我正在从以下格式转换:

const int four_bytes_per_float = 4;
const int eight_bits_per_byte = 8;
_stereoGraphStreamFormat.mFormatID          = kAudioFormatLinearPCM;
_stereoGraphStreamFormat.mFormatFlags       = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
_stereoGraphStreamFormat.mBytesPerPacket    = four_bytes_per_float;
_stereoGraphStreamFormat.mFramesPerPacket   = 1;
_stereoGraphStreamFormat.mBytesPerFrame     = four_bytes_per_float;
_stereoGraphStreamFormat.mChannelsPerFrame  = 2;     
_stereoGraphStreamFormat.mBitsPerChannel    = eight_bits_per_byte * four_bytes_per_float;
_stereoGraphStreamFormat.mSampleRate        = 44100;

格式如下:

interleavedAudioDescription.mFormatID          = kAudioFormatLinearPCM;
interleavedAudioDescription.mFormatFlags       = kAudioFormatFlagIsSignedInteger;
interleavedAudioDescription.mChannelsPerFrame  = 2;
interleavedAudioDescription.mBytesPerPacket    = sizeof(SInt16)*interleavedAudioDescription.mChannelsPerFrame;
interleavedAudioDescription.mFramesPerPacket   = 1;
interleavedAudioDescription.mBytesPerFrame     = sizeof(SInt16)*interleavedAudioDescription.mChannelsPerFrame;
interleavedAudioDescription.mBitsPerChannel    = 8 * sizeof(SInt16);
interleavedAudioDescription.mSampleRate        = 44100;

使用以下代码:

int32_t availableBytes = 0;

void* tailL = TPCircularBufferTail(inputBufferL(), &availableBytes);
void* tailR = TPCircularBufferTail(inputBufferR(), &availableBytes);

// If we have no data in the buffer, we simply return
if (availableBytes <= 0)
{
    return;
}

// ========== Non-Interleaved to Interleaved (Plus Samplerate Conversion) =========

// Get the number of frames available
UInt32 frames = availableBytes / this->mInputFormat.mBytesPerFrame;

pcmOutputBuffer->mBuffers[0].mDataByteSize = frames * interleavedAudioDescription.mBytesPerFrame;

struct complexInputDataProc_t data = (struct complexInputDataProc_t) { .self = this, .sourceL = tailL, .sourceR = tailR, .byteLength = availableBytes };

// Do the conversion
OSStatus result = AudioConverterFillComplexBuffer(interleavedAudioConverter,
                                                  complexInputDataProc,
                                                  &data,
                                                  &frames,
                                                  pcmOutputBuffer,
                                                  NULL);

// Tell the buffers how much data we consumed during the conversion so that it can be removed
TPCircularBufferConsume(inputBufferL(), availableBytes);
TPCircularBufferConsume(inputBufferR(), availableBytes);

// ========== Buffering Of Interleaved Samples =========

// If we got converted frames back from the converter, we want to add it to a separate buffer
if (frames > 0)
{
    // Make sure we have enough space in the buffer to store the new data
    TPCircularBufferHead(&pcmCircularBuffer, &availableBytes);

    if (availableBytes > pcmOutputBuffer->mBuffers[0].mDataByteSize)
    {
        // Add the newly converted data to the buffer
        TPCircularBufferProduceBytes(&pcmCircularBuffer, pcmOutputBuffer->mBuffers[0].mData, frames * interleavedAudioDescription.mBytesPerFrame);
    }
    else
    {
        printf("No Space in Buffer\n");
    }
}

但是我得到以下输出:

它应该是一个完美的正弦波,但如您所见,它不是。

我已经为此工作好几天了,似乎找不到哪里出了问题。 谁能看到我可能遗漏的东西?

编辑:

缓冲区初始化:

TPCircularBuffer        pcmCircularBuffer;
static SInt16           pcmOutputBuf[BUFFER_SIZE];

pcmOutputBuffer = (AudioBufferList*)malloc(sizeof(AudioBufferList));
pcmOutputBuffer->mNumberBuffers = 1;
pcmOutputBuffer->mBuffers[0].mNumberChannels = 2;
pcmOutputBuffer->mBuffers[0].mData = pcmOutputBuf;

TPCircularBufferInit(&pcmCircularBuffer, BUFFER_SIZE);

复杂的输入数据过程:

static OSStatus complexInputDataProc(AudioConverterRef             inAudioConverter,
                                 UInt32                        *ioNumberDataPackets,
                                 AudioBufferList               *ioData,
                                 AudioStreamPacketDescription  **outDataPacketDescription,
                                 void                          *inUserData) {

struct complexInputDataProc_t *arg = (struct complexInputDataProc_t*)inUserData;
BroadcastingServices::MP3Encoder *self = (BroadcastingServices::MP3Encoder*)arg->self;

if ( arg->byteLength <= 0 )
{
    *ioNumberDataPackets = 0;
    return 100; //kNoMoreDataErr;
}

UInt32 framesAvailable = arg->byteLength / self->interleavedAudioDescription.mBytesPerFrame;

if (*ioNumberDataPackets > framesAvailable)
{
    *ioNumberDataPackets = framesAvailable;
}

ioData->mBuffers[0].mData = arg->sourceL;
ioData->mBuffers[0].mDataByteSize = arg->byteLength;

ioData->mBuffers[1].mData = arg->sourceR;
ioData->mBuffers[1].mDataByteSize = arg->byteLength;

arg->byteLength = 0;

return noErr;

}

我认为您的输出缓冲区 pcmCircularBuffer 不够大。

尝试替换

TPCircularBufferInit(&pcmCircularBuffer, BUFFER_SIZE);

TPCircularBufferInit(&pcmCircularBuffer, sizeof(pcmOutputBuf));

即使 解决方案,我认为您的代码存在一些问题。我不确切知道你在做什么,我想编码 mp3(这本身就是 iOS 上的一场艰苦战斗,为什么不使用硬件 AAC?),但除非你对输入和输出都有实时要求,为什么要使用环形缓冲区呢?此外,我建议使用单位来直观地捕捉类型 frame/byte 大小不匹配:例如BUFFER_SIZE_IN_FRAMES

如果不是解决方案,那么我想看看正弦发生器。

我看到了一些危险信号。

1) 如上面的评论所述,您正在用右侧的输入覆盖左侧输入的 availableBytes:

void* tailL = TPCircularBufferTail(inputBufferL(), &availableBytes);
void* tailR = TPCircularBufferTail(inputBufferR(), &availableBytes);

如果两个输入流与此代码异步更改,那么很可能存在竞争条件。

2) 截断错误:availableBytes 不一定是每帧字节数的倍数。如果不是,则以下代码可能会导致您消耗的字节数超过实际转换的字节数。

void* tailL = TPCircularBufferTail(inputBufferL(), &availableBytes);
void* tailR = TPCircularBufferTail(inputBufferR(), &availableBytes);
...
UInt32 frames = availableBytes / this->mInputFormat.mBytesPerFrame;
...
TPCircularBufferConsume(inputBufferL(), availableBytes);
TPCircularBufferConsume(inputBufferR(), availableBytes);

3) 如果输出缓冲区还没有准备好消耗所有输入,您只需将输入缓冲区扔掉。这发生在这段代码中。

if (availableBytes > pcmOutputBuffer->mBuffers[0].mDataByteSize)
{
    ...
}
else
{
    printf("No Space in Buffer\n");
}

如果您看到打印输出,我会非常好奇。

以下是我建议的做法。这将是伪代码,因为我没有任何必要的东西来编译和测试它。

int32_t availableBytesInL = 0;
int32_t availableBytesInR = 0;
int32_t availableBytesOut = 0;

// figure out how many bytes are available in each buffer.
void* tailL = TPCircularBufferTail(inputBufferL(), &availableBytesInL);
void* tailR = TPCircularBufferTail(inputBufferR(), &availableBytesInR);
TPCircularBufferHead(&pcmCircularBuffer, &availableBytesOut);

// figure out how many full frames are available
UInt32 framesInL = availableBytesInL / mInputFormat.mBytesPerFrame;
UInt32 framesInR = availableBytesInR / mInputFormat.mBytesPerFrame;
UInt32 framesOut = availableBytesOut / interleavedAudioDescription.mBytesPerFrame;

// figure out how many frames to process this time.
UInt32 frames = min(min(framesInL, framesInL), framesOut);

if (frames == 0)
    return;

int32_t bytesConsumed = frames * mInputFormat.mBytesPerFrame;

struct complexInputDataProc_t data = (struct complexInputDataProc_t) {
    .self = this, .sourceL = tailL, .sourceR = tailR, .byteLength = bytesConsumed };

// Do the conversion
OSStatus result = AudioConverterFillComplexBuffer(interleavedAudioConverter,
                                                  complexInputDataProc,
                                                  &data,
                                                  &frames,
                                                  pcmOutputBuffer,
                                                  NULL);

int32_t bytesProduced = frames * interleavedAudioDescription.mBytesPerFrame;

// Tell the buffers how much data we consumed during the conversion so that it can be removed
TPCircularBufferConsume(inputBufferL(), bytesConsumed);
TPCircularBufferConsume(inputBufferR(), bytesConsumed);
TPCircularBufferProduceBytes(&pcmCircularBuffer, pcmOutputBuffer->mBuffers[0].mData, bytesProduced);

基本上我在这里所做的是预先计算出应该处理多少帧,确保我只处理输出缓冲区可以处理的帧数。如果是我,我还会添加一些检查输出缓冲区欠载和输入缓冲区溢出的检查。最后,我不确定 AudioConverterFillComplexBuffer 与传入和传出的 frame 参数的语义。可以想象,输出的#帧数将少于或多于输入的帧数。尽管如此,由于您没有进行采样率转换,所以这可能不会发生。我试图通过在转换后分配 bytesProduced 来解释这种情况。

希望这对您有所帮助。如果不是,您还有其他 2 条线索。一是辍学是周期性的,二是辍学的规模看起来大致相同。如果您能算出每个样本有多少个样本,那么您可以在代码中查找这些数字。