我目前正在编写一个大致包含以下内容的C ++实时音频应用程序:
我认为这应该适用于我的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]);
}
答案 0 :(得分:4)
关于绩效改进的一些建议:
查看对大量数据(例如数组)进行操作的函数。这些函数应该将数据加载到缓存中,对数据进行操作,然后存储回内存。
应该组织数据以最好地适应数据缓存。如果数据不合适,则将数据分解为更小的块。在网上搜索&#34;数据驱动设计&#34;和#34;缓存优化&#34;。
在一个项目中,执行数据平滑,我改变了数据布局并获得了70%的性能。
总体而言,您可以使用至少三个专用线程:输入,处理和输出。输入线程获取数据并将其存储在缓冲区中;在网上搜索&#34;双缓冲&#34;。第二个线程从输入缓冲区获取数据,处理它,然后写入输出缓冲区。第三个线程将数据从输出缓冲区写入文件。
您也可以从左侧和右侧样本使用线程中受益。例如,当一个线程正在处理左侧样本时,另一个线程可能正在处理正确的样本。如果您可以将线程放在不同的核心上,您可能会看到更多的性能优势。
许多现代图形处理单元(GPU)都有许多可以处理浮点值的内核。也许你可以将一些过滤或分析功能委托给GPU中的核心。请注意,这需要开销并获得好处,处理部分应该比开销更具计算性。
处理器更喜欢通过分支来操纵数据。分支停止执行,因为处理器必须找出获取和处理下一条指令的位置。有些具有可以包含小循环的大型指令缓存;但是再次分支到循环顶部仍然存在惩罚。请参阅&#34;循环展开&#34;。还要检查编译器优化并优化性能。如果情况正确,许多编译器将切换到循环展开。
您是否需要处理整个样本或部分样本?例如,在视频处理中,帧的大部分不会仅改变小部分。因此整个框架不需要处理。音频通道是否可以隔离,因此只处理几个通道而不是整个频谱?
您可以使用const
修饰符来帮助编译器进行优化。编译器可能能够针对不变的变量使用不同的算法。例如,const
值可以放在可执行代码中,但non-const
值必须放在内存中。
使用static
和const
也可以提供帮助。 static
通常只暗示一个实例。 const
暗示了一些不会发生变化的事情。因此,如果只有一个变量实例没有变化,编译器可以将其放入可执行或只读存储器中,并执行更高的代码优化。
同时加载多个变量也有帮助。处理器可以将数据放入缓存中。编译器可能能够使用专门的汇编指令来获取顺序数据。