实时音频应用程序,提高性能

Realtime audio application, improving performance

本文关键字:高性能 应用程序 音频 实时      更新时间:2023-10-16

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

  • 从缓冲区读取帧
  • 在这里使用隐式插值对帧进行插值
  • 用两个双四阶滤波器对每帧进行滤波(并每帧更新其系数)
  • 一个包含18个双象限计算的3波段交叉
  • STK库中的FreeVerb算法

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

1)操作员过载

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

滤波器(主要是双声道)和混响使用浮点,不使用此类,但hermite插值器以及用于音量控制和混音的每一次乘法和加法都使用该类。

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

2)标准::函数

音频IO库PortAudio的回调函数调用std::函数。我用它来封装与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]); 
}

关于性能改进的一些建议:

优化数据缓存使用率

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

应将数据组织为最适合数据缓存的数据。如果数据不合适,就把它分解成更小的块。在网上搜索"数据驱动设计"answers"缓存优化"。

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

使用多个线程

从全局来看,您可能能够使用至少三个专用线程:输入、处理和输出。输入线程获取数据并将其存储在缓冲区中;在Web上搜索"双重缓冲"。第二个线程从输入缓冲区获取数据,对其进行处理,然后写入输出缓冲区。第三个线程将数据从输出缓冲区写入文件。

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

使用GPU处理

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

减少分支

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

减少处理量

你需要处理整个样品还是部分样品?例如,在视频处理中,大部分帧不会只改变很小的部分。所以不需要处理整个帧。音频通道是否可以隔离,以便只处理少数通道而不是整个频谱?

帮助编译器优化的编码

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

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

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