如果要渲染的数据太多,双簧管将停止渲染音频
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。 OggPlayer
是 PlayerQueue
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());
}
这是我目前渲染音频数据的方式,我遍历每个OggPlayer
的PlayerQueue
向量,如果它们没有到达终点,通过传递数组指针让它们渲染音频数据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秒,足够等待队列清空的时间)后又开始渲染了。所以我有几个问题
- 像上面这种情况,如果渲染音频花费的时间太长,Oboe 是否会停止渲染音频?
- 我是否达到了渲染音频的限制,这意味着只限制队列数是解决方案?或者有什么方法可以达到更好的性能?
这些代码在我的 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
- 剖析回调中的代码 - 确定您花费最多时间的地方。
- 您似乎对
sin
和 cos
进行了多次调用。这些函数可能有更快的版本。
我谈到了其中一些 debugging/optimisation 技术 in this talk
另一个快速提示。除非你真的别无选择,否则尽量避免使用原始指针。例如 AudioStream* stream;
会更好,因为 std::shared_ptr<AudioStream>
和 std::vector<OggPlayer>* players;
可以重构为 std::vector<OggPlayer> players;
我正在尝试将 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。 OggPlayer
是 PlayerQueue
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());
}
这是我目前渲染音频数据的方式,我遍历每个OggPlayer
的PlayerQueue
向量,如果它们没有到达终点,通过传递数组指针让它们渲染音频数据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秒,足够等待队列清空的时间)后又开始渲染了。所以我有几个问题
- 像上面这种情况,如果渲染音频花费的时间太长,Oboe 是否会停止渲染音频?
- 我是否达到了渲染音频的限制,这意味着只限制队列数是解决方案?或者有什么方法可以达到更好的性能?
这些代码在我的 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
- 剖析回调中的代码 - 确定您花费最多时间的地方。
- 您似乎对
sin
和cos
进行了多次调用。这些函数可能有更快的版本。
我谈到了其中一些 debugging/optimisation 技术 in this talk
另一个快速提示。除非你真的别无选择,否则尽量避免使用原始指针。例如 AudioStream* stream;
会更好,因为 std::shared_ptr<AudioStream>
和 std::vector<OggPlayer>* players;
可以重构为 std::vector<OggPlayer> players;