如果要渲染的数据太多,双簧管将停止渲染音频

Oboe stops rendering audio if there is too many data to be rendered

我正在尝试将 Oboe 库实现到我的应用程序中,这样我就可以执行低延迟音频播放。我可以执行平移、播放操作、声音缩放等。我一直在问关于这个主题的几个问题,因为我对音频世界完全陌生。

现在我可以执行内部 Android 音频 class 例如 SoundPool 提供的基本操作。我可以同时播放多个声音而没有明显的延迟。

但是现在又遇到了一个问题。所以我做了一个非常简单的应用程序;屏幕上有一个按钮,如果用户点击这个屏幕,它会播放简单的钢琴声。无论用户点击此按钮的速度有多快,它都必须能够像 SoundPool 那样混合那些相同的钢琴音色。

我的代码可以很好地做到这一点,直到我点击按钮太多次,所以有很多音频 queues 要混合。

class OggPlayer;

class PlayerQueue {
private:
    OggPlayer* player;

    void renderStereo(float* audioData, int32_t numFrames);
    void renderMono(float* audioData, int32_t numFrames);
public:
    int offset = 0;
    float pan;
    float pitch;
    int playScale;

    bool queueEnded = false;

    PlayerQueue(float pan, float pitch, int playScale, OggPlayer* player) {
        this->pan = pan;
        this->playScale = playScale;
        this->player = player;
        this->pitch = pitch;

        if(this->pan < -1.0)
            this->pan = -1.0;
        else if(this->pan > 1.0)
            this->pan = 1.0;
    }

    void renderAudio(float* audioData, int32_t numFrames, bool isStreamStereo);
};

class OggPlayer {
private:
    std::vector<PlayerQueue> queues = std::vector<PlayerQueue>();
public:

    int offset = 0;
    bool isStereo;
    float defaultPitch = 1.0;

    OggPlayer(std::vector<float> data, bool isStereo, int fileSampleRate, int deviceSampleRate) {
        this->data = data;
        this->isStereo = isStereo;
        defaultPitch = (float) (fileSampleRate) / (float) (deviceSampleRate);
    }

    void renderAudio(float* audioData, int32_t numFrames, bool reset, bool isStreamStereo);
    static void smoothAudio(float* audioData, int32_t numFrames, bool isStreamStereo);

    void addQueue(float pan, float pitch, int playerScale) {
        queues.push_back(PlayerQueue(pan, defaultPitch * pitch, playerScale, this));
    };

    static void resetAudioData(float* audioData, int32_t numFrames, bool isStreamStereo);

    std::vector<float> data;
};

OggPlayer 保存具有 defulatPitch 值的解码 PCM 数据,以同步扬声器的采样率和音频文件的采样率。每个 OggPlayer 都拥有自己的 PCM 数据(即每个音频文件的数据),并且拥有自己的 PlayerQueue 向量。 PlayerQueue 是实际呈现音频数据的 class。 OggPlayerPlayerQueue classes 的 PCM 数据提供者。 PlayerQueue 有自己的自定义音高、声像和音频比例值。由于 AudioStream 可以在回调方法中提供有限大小的数组,我添加了 offset,因此 PlayerQueue 可以在下一个会话中继续渲染音频而不会丢失其状态。

void OggPlayer::renderAudio(float *audioData, int32_t numFrames, bool reset, bool isStreamStereo) {
    if(reset) {
        resetAudioData(audioData, numFrames, isStreamStereo);
    }

    for(auto & queue : queues) {
        if(!queue.queueEnded) {
            queue.renderAudio(audioData, numFrames, isStreamStereo);
        }
    }

    smoothAudio(audioData, numFrames, isStreamStereo);

    queues.erase(std::remove_if(queues.begin(), queues.end(),
            [](const PlayerQueue& p) {return p.queueEnded;}), queues.end());
}

这是我目前渲染音频数据的方式,我遍历每个OggPlayerPlayerQueue向量,如果它们没有到达终点,通过传递数组指针让它们渲染音频数据PCM数据阵列呢。我在完成音频数据后平滑音频数据,以防止剪辑或其他事情。然后,如果他们完成了音频渲染(完全),最后从 vector 中移除队列。

void PlayerQueue::renderAudio(float * audioData, int32_t numFrames, bool isStreamStereo) {
    if(isStreamStereo) {
        renderStereo(audioData, numFrames);
    } else {
        renderMono(audioData, numFrames);
    }
}

void PlayerQueue::renderStereo(float *audioData, int32_t numFrames) {
    for(int i = 0; i < numFrames; i++) {
        if(player->isStereo) {
            if((int) ((float) (offset + i) * pitch) * 2 + 1 < player->data.size()) {
                float left = player->data.at((int)((float) (offset + i) * pitch) * 2);
                float right = player->data.at((int)((float) (offset + i) * pitch)  * 2 + 1);

                if(pan < 0) {
                    audioData[i * 2] += (left + right * (float) sin(abs(pan) * M_PI / 2.0)) * (float) playScale;
                    audioData[i * 2 + 1] += right * (float) cos(abs(pan) * M_PI / 2.0) * (float) playScale;
                } else {
                    audioData[i * 2] += left * (float) cos(pan * M_PI / 2.0) * (float) playScale;
                    audioData[i * 2 + 1] += (right + left * (float) sin(pan * M_PI / 2.0)) * (float) playScale;
                }
            } else {
                break;
            }
        } else {
            if((int) ((float) (offset + i) * pitch) < player->data.size()) {
                float sample = player->data.at((int) ((float) (offset + i) * pitch));

                if(pan < 0) {
                    audioData[i * 2] += sample * (1 + (float) sin(abs(pan) * M_PI / 2.0)) * (float) playScale;
                    audioData[i * 2 + 1] += sample * (float) cos(abs(pan) * M_PI / 2.0) * (float) playScale;
                } else {
                    audioData[i * 2] += sample * (float) cos(pan * M_PI / 2.0) * (float) playScale;
                    audioData[i * 2 + 1] += sample * (1 + (float) sin(pan * M_PI / 2.0)) * (float) playScale;
                }
            } else {
                break;
            }
        }
    }

    offset += numFrames;

    if((float) offset * pitch >= player->data.size()) {
        offset = 0;
        queueEnded = true;
    }
}

void PlayerQueue::renderMono(float *audioData, int32_t numFrames) {
    for(int i = 0; i < numFrames; i++) {
        if(player->isStereo) {
            if((int) ((float) (offset + i) * pitch) * 2 + 1 < player->data.size()) {
                audioData[i] += (player->data.at((int) ((float) (offset + i) * pitch) * 2) + player->data.at((int) ((float) (offset + i) * pitch) * 2 + 1)) / 2 * (float) playScale;
            } else {
                break;
            }
        } else {
            if((int) ((float) (offset + i) * pitch) < player->data.size()) {
                audioData[i] += player->data.at((int) ((float) (offset + i) * pitch)) * (float) playScale;
            } else {
                break;
            }
        }

        if(audioData[i] > 1.0)
            audioData[i] = 1.0;
        else if(audioData[i] < -1.0)
            audioData[i] = -1.0;
    }

    offset += numFrames;

    if((float) offset * pitch >= player->data.size()) {
        queueEnded = true;
        offset = 0;
    }
}

考虑到扬声器和音频文件的状态(单声道或立体声),我在一个会话中渲染队列中的所有内容(平移、播放、缩放)

using namespace oboe;

class OggPianoEngine : public AudioStreamCallback {
public:
    void initialize();
    void start(bool isStereo);
    void closeStream();
    void reopenStream();
    void release();

    bool isStreamOpened = false;
    bool isStreamStereo;

    int deviceSampleRate = 0;

    DataCallbackResult
    onAudioReady(AudioStream *audioStream, void *audioData, int32_t numFrames) override;
    void onErrorAfterClose(AudioStream *audioStream, Result result) override ;

    AudioStream* stream;
    std::vector<OggPlayer>* players;

    int addPlayer(std::vector<float> data, bool isStereo, int sampleRate) const;

    void addQueue(int id, float pan, float pitch, int playerScale) const;
};

然后最后在 OggPianoEngine 中,我放置了 OggPlayer 的向量,这样我的应用程序可以在内存中保存多个声音,使用户能够添加声音,也能够在任何地方播放它们, 随时.

DataCallbackResult
OggPianoEngine::onAudioReady(AudioStream *audioStream, void *audioData, int32_t numFrames) {
    for(int i = 0; i < players->size(); i++) {
        players->at(i).renderAudio(static_cast<float*>(audioData), numFrames, i == 0, audioStream->getChannelCount() != 1);
    }

    return DataCallbackResult::Continue;
}

在引擎中渲染音频非常简单,如你所料,我只是通过OggPlayer的矢量进行搜索,然后调用renderAudio方法。下面的代码是我如何初始化 AudioStream.

void OggPianoEngine::start(bool isStereo) {
    AudioStreamBuilder builder;

    builder.setFormat(AudioFormat::Float);
    builder.setDirection(Direction::Output);
    builder.setChannelCount(isStereo ? ChannelCount::Stereo : ChannelCount::Mono);
    builder.setPerformanceMode(PerformanceMode::LowLatency);
    builder.setSharingMode(SharingMode::Exclusive);

    builder.setCallback(this);

    builder.openStream(&stream);

    stream->setBufferSizeInFrames(stream->getFramesPerBurst() * 2);

    stream->requestStart();

    deviceSampleRate = stream->getSampleRate();

    isStreamOpened = true;
    isStreamStereo = isStereo;
}

因为我看过双簧管的基本视频指南this or this,所以我尝试配置LowLatency模式的基本设置(例如,将设置缓冲区大小设置为突发大小乘以2)。但是当队列太多时,音频开始停止渲染。起初,声音开始断断续续。感觉就像它跳过了一些渲染会话,然后如果我在此之后再点击播放按钮,它就会完全停止渲染。我等了一会儿(5~10秒,足够等待队列清空的时间)后又开始渲染了。所以我有几个问题

  1. 像上面这种情况,如果渲染音频花费的时间太长,Oboe 是否会停止渲染音频?
  2. 我是否达到了渲染音频的限制,这意味着只限制队列数是解决方案?或者有什么方法可以达到更好的性能?

这些代码在我的 flutter 插件中,所以你可以从这里获得完整的代码 github link

Does Oboe stop rendering audio if it takes too much time to render audio like situation above?

是的。如果您阻止 onAudioReady 的时间超过 numFrames 表示的时间,您将遇到音频故障。我敢打赌,如果你 运行 systrace.py --time=5 -o trace.html -a your.app.packagename audio sched freq 你会发现你在该方法中花费了太多时间。

Did I reach limit of rendering audio, meaning that only limiting number of queues is the solution? or are there any ways to reach better performance?

看起来像。问题是你试图在音频回调中做太多的工作。我会立即尝试的事情:

  • 不同的编译器优化:尝试-O2-O3-Ofast
  • 剖析回调中的代码 - 确定您花费最多时间的地方。
  • 您似乎对 sincos 进行了多次调用。这些函数可能有更快的版本。

我谈到了其中一些 debugging/optimisation 技术 in this talk

另一个快速提示。除非你真的别无选择,否则尽量避免使用原始指针。例如 AudioStream* stream; 会更好,因为 std::shared_ptr<AudioStream>std::vector<OggPlayer>* players; 可以重构为 std::vector<OggPlayer> players;