音频流?那是什么?
音频流与音乐类似(还记得sf::Music类吗?)。它几乎具有相同的函数和行为。唯一的区别是音频流不播放音频文件:相反,它播放您直接提供的自定义音频源。换句话说,定义自己的音频流允许您播放不仅限于文件的内容:从网络流式传输的声音,由您的程序生成的音乐,SFML不支持的音频格式等等。
事实上,sf::Music类只是一个专门的音频流,它从文件中获取其音频样本。
由于我们正在讨论流式传输,因此我们将处理无法完全加载到内存中的音频数据,并在播放时加载小块。如果您的声音可以完全加载并且可以适合内存,则音频流将无助于您:只需将音频数据加载到sf::SoundBuffer中,然后使用常规的sf::Sound来播放它。
sf::SoundStream
为了定义自己的音频流,您需要继承sf::SoundStream抽象基类。在您的派生类中有两个要重写的虚拟函数:onGetData和onSeek。
class MyAudioStream : public sf::SoundStream
{
virtual bool onGetData(Chunk& data);
virtual void onSeek(sf::Time timeOffset);
};
当基类用尽音频样本并需要更多音频样本时,onGetData会被调用。您需要通过填充data参数来提供新的音频样本:
bool MyAudioStream::onGetData(Chunk& data)
{
data.samples = /* put the pointer to the new audio samples */;
data.sampleCount = /* put the number of audio samples available in the new chunk */;
return true;
}
当一切正常时,您必须返回true,如果发生错误或者没有更多的音频数据播放,则必须返回false以停止播放。
SFML会在onGetData返回时立即对音频样本进行内部拷贝,因此如果您不想保留原始数据,则无需保留。
当调用setPlayingOffset公共函数时,onSeek函数会被调用。它的目的是改变源数据的当前播放位置。参数是一个时间值,表示新位置,从声音的开头开始计算(而不是从当前位置开始)。在某些情况下,此函数可能无法实现。在这种情况下,请留空,并告诉您类的用户不支持更改播放位置。
现在,您的类几乎已经准备好工作了。SFML现在需要知道您的流的通道数和采样率,以便可以按预期进行播放。为了让基类知道这些参数,您必须在流类中尽快调用受保护的函数initialize,以便在流加载 / 初始化时知道它们。
// where this is done totally depends on how your stream class is designed
unsigned int channelCount = ...;
unsigned int sampleRate = ...;
initialize(channelCount, sampleRate);
线程问题
音频流始终在单独的线程中播放,因此了解发生的事情以及发生的位置非常重要。
onSeek直接由setPlayingOffset函数调用,因此它始终在调用线程中执行。但是,只要流正在播放,onGetData函数将被重复调用,在SFML创建的单独的线程中执行。如果您的流使用可能会同时在调用线程和播放线程中访问的数据,则必须保护它(例如使用互斥锁),以避免并发访问,这可能会导致未定义的行为 - 播放损坏的数据,崩溃等等。
如果您对线程不太熟悉,可以参考相应的教程获取更多信息。
使用你的音频流
现在您已经定义了自己的音频流类,让我们看看如何使用它。实际上,与关于sf :: Music的教程所示的非常相似。您可以使用play,pause,stop和setPlayingOffset函数控制播放。您还可以设置声音的属性,例如音量或音调。您可以参考API文档或其他音频教程以获取更多详细信息。
一个简单例子
下面是一个非常简单的自定义音频流类示例,它播放声音缓冲区的数据。这样的类可能看起来完全没有用,但重点是关注该类如何流式传输数据,而不管数据来自何处。
#include <SFML/Audio.hpp>
#include <vector>
// custom audio stream that plays a loaded buffer
class MyStream : public sf::SoundStream
{
public:
void load(const sf::SoundBuffer& buffer)
{
// extract the audio samples from the sound buffer to our own container
m_samples.assign(buffer.getSamples(), buffer.getSamples() + buffer.getSampleCount());
// reset the current playing position
m_currentSample = 0;
// initialize the base class
initialize(buffer.getChannelCount(), buffer.getSampleRate());
}
private:
virtual bool onGetData(Chunk& data)
{
// number of samples to stream every time the function is called;
// in a more robust implementation, it should be a fixed
// amount of time rather than an arbitrary number of samples
const int samplesToStream = 50000;
// set the pointer to the next audio samples to be played
data.samples = &m_samples[m_currentSample];
// have we reached the end of the sound?
if (m_currentSample + samplesToStream <= m_samples.size())
{
// end not reached: stream the samples and continue
data.sampleCount = samplesToStream;
m_currentSample += samplesToStream;
return true;
}
else
{
// end of stream reached: stream the remaining samples and stop playback
data.sampleCount = m_samples.size() - m_currentSample;
m_currentSample = m_samples.size();
return false;
}
}
virtual void onSeek(sf::Time timeOffset)
{
// compute the corresponding sample index according to the sample rate and channel count
m_currentSample = static_cast<std::size_t>(timeOffset.asSeconds() * getSampleRate() * getChannelCount());
}
std::vector<sf::Int16> m_samples;
std::size_t m_currentSample;
};
int main()
{
// load an audio buffer from a sound file
sf::SoundBuffer buffer;
buffer.loadFromFile("sound.wav");
// initialize and play our custom stream
MyStream stream;
stream.load(buffer);
stream.play();
// let it play until it is finished
while (stream.getStatus() == MyStream::Playing)
sf::sleep(sf::seconds(0.1f));
return 0;
}