【问题标题】:Realtime audio application, improving performance实时音频应用,提升性能
【发布时间】:2015-01-06 01:59:16
【问题描述】:

我目前正在编写一个 C++ 实时音频应用程序,它大致包含:

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

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

1) 运算符重载

而不是直接使用我的浮动样本并为每个样本进行计算, 我将我的浮点数打包在一个包含左右样本的Frame 类中。该类使用float 重载了一些用于加法、减法和乘法的运算符。

过滤器(主要是双二阶)和混响与浮点数一起使用,不使用此类,但 Hermite 插值器以及用于音量控制和混合的每个乘法和加法都使用该类。

这对性能有影响吗?直接使用左右样本会更好吗?

2) std::function

来自音频 IO 库 PortAudio 的回调函数调用 std::function。我用它来封装与 PortAudio 相关的所有内容。所以“用户”用 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]); 
}

【问题讨论】:

  • 这些语言结构不会成为瓶颈。很有可能您的音频处理速度很慢,或者您执行动态分配 (new Frame) 之类的操作比您需要的频率高得多。
  • 我没有在回调中分配任何东西,我使用现有的缓冲区。我想我会添加所有相关功能,以便您检查处理。 ;)
  • 您应该始终在优化之前进行分析。这将有助于隔离需要性能优化的代码区域。
  • 是否有部分代码可以并行执行?例如,一个线程处理左侧样本,另一个线程处理右侧样本。另外,看看您是否可以利用图形处理单元的处理能力。
  • 是的,均衡器处理可以拆分为左右,但我不知道是否值得分配一个新线程。 (但是这会起作用......)

标签: c++ performance audio real-time


【解决方案1】:

关于性能提升的一些建议:

优化数据缓存使用

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

应组织数据以最适合数据缓存。如果不合适,将数据分解成更小的块。在网上搜索“数据驱动设计”和“缓存优化”。

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

使用多线程

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

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

使用 GPU 处理

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

减少分支

处理器更喜欢操作数据而不是分支。由于处理器必须弄清楚从哪里获取和处理下一条指令,因此分支会停止执行。有些具有可以包含小循环的大型指令缓存;但是再次分支到循环顶部仍然会受到惩罚。请参阅“循环展开”。还要检查您的编译器优化并优化高性能。如果情况正确,许多编译器将为您切换到循环展开。

减少处理量

您需要处理整个样本还是部分样本?例如,在视频处理中,大部分帧不会只改变一小部分。所以不需要对整个帧进行处理。是否可以隔离音频通道以便只处理几个通道而不是整个频谱?

帮助编译器优化的编码

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

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

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

【讨论】:

  • 您好,感谢您的回复。我有一些问题:整个处理过程分为两部分,因此在不同的线程中处理这些部分确实会产生影响。但是我应该如何实现呢? (框架?c++ std 还是其他东西?)我从来没有听说过优化缓存使用,但我会读到这个主题。 :) 感谢您的提示!
  • 请澄清“我应该如何实现这个”?你问如何实现线程?线程通常是特定于平台的问题,但请查看Boost::threads 或搜索“Boost library threading”
  • 是的,这就是问题所在。 :) 我并没有真正使用线程期望在某些诅咒中,但在大多数情况下,我们使用线程是为了了解它们的工作方式,而不是因此我们从它们中获得任何(例如)性能改进。 ;D
  • 我刚刚通过让单独的线程处理左音轨将性能提高到 150%。 std::thread 做得很好。 :D
  • 使用您的应用程序,猜测性能瓶颈在哪里很棘手。瓶颈可能是文件 I/O(通常是大多数 I/O 基础程序),也可能是数据分析(常见于数据密集型程序)。我建议您检查您的输入和输出功能。如果可能的话,目标是不让主程序等待 I/O 或让数据提供者等待(就像硬盘驱动器一样)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-19
  • 2021-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多