实时音频应用,提高性能

时间:2014-11-09 19:34:43

标签: c++ performance audio real-time

我目前正在编写一个大致包含以下内容的C ++实时音频应用程序:

  • 从缓冲区读取框架
  • 使用隐士插值here
  • 插值帧
  • 使用两个双二阶滤波器过滤帧(并且每帧更新它们的系数)
  • 包含18个双二阶计算的3波段交叉
  • 来自STK libary here
  • 的FreeVerb算法

我认为这应该适用于我的PC,但我经常会遇到一些缓冲区下溢,所以我想提高应用程序的性能。我有一堆问题,希望你能回答我。 :)

1)运算符重载

不是直接使用我的flaot样本并对每个样本进行计算, 我将我的花车包装在Frame类中,其中包含左侧和右侧样本。该类使用float重载一些运算符以进行加法,减法和乘法。

过滤器(大部分为双向)和混响使用浮点数并且不使用此类,但是hermite插值器以及音量控制和混音的每次乘法和加法都使用该类。

这是否会对性能产生影响?是否可以直接使用左右样本?

2)std :: function

来自音频IO库的回调函数PortAudio调用std :: function。我使用它来封装与PortAudio相关的所有内容。所以“user”用std :: bind

设置自己的回调函数
std::bind(  &AudioController::processAudio, 
            &(*this), 
            std::placeholders::_1, 
            std::placeholders::_2));

因为对于每个回调,必须从CPU找到正确的函数(但这有效...),这是否会产生影响,并且定义用户必须继承的类会更好吗?

3)虚拟功能

我使用一个名为AudioProcessor的类来声明一个虚函数:

virtual void tick(Frame *buffer, int frameCout) = 0;

此功能始终一次处理多个帧。根据驱动器的不同,每次通话200帧,最多1000帧。 在信号处理路径中,我从多个派生类中调用此函数6次。我记得这是通过查找表完成的,因此CPU确切地知道它必须调用哪个函数。那么调用“虚拟”(派生)函数的过程对性能有影响吗?

关于这一点的好处是源代码中的结构,但只使用内联可能会提高性能。

这些都是目前的问题。我有更多关于Qt事件循环的信息,因为我认为我的GUI也使用了相当多的CPU时间。但这是我猜的另一个话题。 :)

提前致谢!


这些都是信号处理中的相关函数调用。其中一些来自STK图书馆。 双二阶函数来自STK,应该表现良好。这也适用于freeverb算法。

// ################################ AudioController Function ############################
void AudioController::processAudio(int frameCount, float *output) {
    // CALCULATE LEFT TRACK

    Frame * leftFrameBuffer = (Frame*) output;

    if(leftLoaded) { // the left processor is loaded
        leftProcessor->tick(leftFrameBuffer, frameCount);   //(TrackProcessor::tick()
    } else {
        for(int i = 0; i < frameCount; i++) {
            leftFrameBuffer[i].leftSample  = 0.0f;
            leftFrameBuffer[i].rightSample = 0.0f;
        }
    }

    // CALCULATE RIGHT TRACk

    if(rightLoaded) { // the right processor is loaded
        // the rightFrameBuffer is allocated once and ensured to have enough space for frameCount Frames
        rightProcessor->tick(rightFrameBuffer, frameCount); //(TrackProcessor::tick()
    } else {
        for(int i = 0; i < frameCount; i++) {
            rightFrameBuffer[i].leftSample  = 0.0f;
            rightFrameBuffer[i].rightSample = 0.0f;
        }
    }

    // MIX
    for(int i = 0; i < frameCount; i++ ) {
        leftFrameBuffer[i] = volume * (leftRightMix * leftFrameBuffer[i] + (1.0 - leftRightMix) * rightFrameBuffer[i]);
    }
}

// ################################ AudioController Function ############################

void TrackProcessor::tick(Frame *frames, int frameNum) {
    if(bufferLoaded && playback) {
        for(int i = 0; i < frameNum; i++) {
            // read from buffer
            frames[i] =  bufferPlayer->tick();

            // filter coeffs
            caltulateFilterCoeffs(lowCutoffFilter->tick(), highCutoffFilter->tick());

            // filter
            frames[i].leftSample = lpFilterL->tick(hpFilterL->tick(frames[i].leftSample));
            frames[i].rightSample = lpFilterR->tick(hpFilterR->tick(frames[i].rightSample));
        }
    } else {
        for(int i = 0; i < frameNum; i++) {         
            frames[i] = Frame(0,0);
        }
    }

    // Effect 1, Equalizer
    if(effsActive[0]) {
        insEffProcessors[0]->tick(frames, frameNum);
    }
    // Effect 2, Reverb
    if(effsActive[1]) {
        insEffProcessors[1]->tick(frames, frameNum);
    }

    // Volume
    for(int i = 0; i < frameNum; i++) {
        frames[i].leftSample  *= volume;
        frames[i].rightSample *= volume;
    }
}

// ################################ Equalizer ############################

void EqualizerProcessor::tick(Frame *frames, int frameNum) {
    if(active) {
        Frame lowCross;
        Frame highCross;

        for(int f = 0; f < frameNum; f++) {

            lowAmp = lowAmpFilter->tick();
            midAmp = midAmpFilter->tick();
            highAmp = highAmpFilter->tick();

            lowCross =  highLPF->tick(frames[f]);
            highCross = highHPF->tick(frames[f]);

            frames[f] = lowAmp * lowLPF->tick(lowCross) 
                      + midAmp * lowHPF->tick(lowCross) 
                      + highAmp * lowAPF->tick(highCross);
        }
    }
}

// ################################ Reverb ############################
// This function just calls the stk::FreeVerb tick function for every frame
// The FreeVerb implementation can't realy be optimised so I will take it as it is.

void ReverbProcessor::tick(Frame *frames, int frameNum) {
    if(active) {
        for(int i = 0; i < frameNum; i++) {
            frames[i].leftSample = reverb->tick(frames[i].leftSample, frames[i].rightSample);
            frames[i].rightSample = reverb->lastOut(1);
        }
    }
}

// ################################ Buffer Playback (BufferPlayer) ############################

Frame BufferPlayer::tick() {
    // adjust read position based on loop status
    if(inLoop) {
        while(readPos > loopEndPos) {
            readPos = loopStartPos + (readPos - loopEndPos); 
        }
    }

    int x1  = readPos;
    float t = readPos - x1;

    Frame f = interpolate(buffer->frameAt(x1-1), 
                          buffer->frameAt(x1),
                          buffer->frameAt(x1+1),
                          buffer->frameAt(x1+2),
                          t);

    readPos += stepSize;;
    return f;
}

// interpolation:
Frame BufferPlayer::interpolate(Frame x0, Frame x1, Frame x2, Frame x3, float t) {
    Frame c0 = x1;
    Frame c1 = 0.5f * (x2 - x0);
    Frame c2 = x0 - (2.5f * x1) + (2.0f * x2) - (0.5f * x3);
    Frame c3 = (0.5f * (x3 - x0)) + (1.5f * (x1 - x2));
    return (((((c3 * t) + c2) * t) + c1) * t) + c0;
}


inline Frame BufferPlayer::frameAt(int pos) {
    if(pos < 0) {
        pos = 0;
    } else if (pos >= frames) {
        pos = frames -1;
    }

    // get chunk and relative Sample
    int chunk = pos/ChunkSize;
    int chunkSample = pos%ChunkSize;

    return Frame(leftChunks[chunk][chunkSample], rightChunks[chunk][chunkSample]); 
}

1 个答案:

答案 0 :(得分:4)

关于绩效改进的一些建议:

优化数据缓存使用

查看对大量数据(例如数组)进行操作的函数。这些函数应该将数据加载到缓存中,对数据进行操作,然后存储回内存。

应该组织数据以最好地适应数据缓存。如果数据不合适,则将数据分解为更小的块。在网上搜索&#34;数据驱动设计&#34;和#34;缓存优化&#34;。

在一个项目中,执行数据平滑,我改变了数据布局并获得了70%的性能。

使用多个线程

总体而言,您可以使用至少三个专用线程:输入,处理和输出。输入线程获取数据并将其存储在缓冲区中;在网上搜索&#34;双缓冲&#34;。第二个线程从输入缓冲区获取数据,处理它,然后写入输出缓冲区。第三个线程将数据从输出缓冲区写入文件。

您也可以从左侧和右侧样本使用线程中受益。例如,当一个线程正在处理左侧样本时,另一个线程可能正在处理正确的样本。如果您可以将线程放在不同的核心上,您可能会看到更多的性能优势。

使用GPU处理

许多现代图形处理单元(GPU)都有许多可以处理浮点值的内核。也许你可以将一些过滤或分析功能委托给GPU中的核心。请注意,这需要开销并获得好处,处理部分应该比开销更具计算性。

减少分支

处理器更喜欢通过分支来操纵数据。分支停止执行,因为处理器必须找出获取和处理下一条指令的位置。有些具有可以包含小循环的大型指令缓存;但是再次分支到循环顶部仍然存在惩罚。请参阅&#34;循环展开&#34;。还要检查编译器优化并优化性能。如果情况正确,许多编译器将切换到循环展开。

减少处理量

您是否需要处理整个样本或部分样本?例如,在视频处理中,帧的大部分不会仅改变小部分。因此整个框架不需要处理。音频通道是否可以隔离,因此只处理几个通道而不是整个频谱?

编码以帮助编译器优化

您可以使用const修饰符来帮助编译器进行优化。编译器可能能够针对不变的变量使用不同的算法。例如,const值可以放在可执行代码中,但non-const值必须放在内存中。

使用staticconst也可以提供帮助。 static通常只暗示一个实例。 const暗示了一些不会发生变化的事情。因此,如果只有一个变量实例没有变化,编译器可以将其放入可执行或只读存储器中,并执行更高的代码优化。

同时加载多个变量也有帮助。处理器可以将数据放入缓存中。编译器可能能够使用专门的汇编指令来获取顺序数据。