Custom audio streams
Audio stream? What's that?
An audio stream is similar to music (remember the sf::Music
class?).
It has almost the same functions and behaves the same.
The only difference is that an audio stream doesn't play an audio file: Instead, it plays a custom audio source that you directly provide.
In other words, defining your own audio stream allows you to play from more than just a file: A sound streamed over the network, music generated by your program, an audio format that SFML doesn't support, etc.
In fact, the sf::Music
class is just a specialized audio stream that gets its audio samples from a file.
Since we're talking about streaming, we'll deal with audio data that cannot be loaded entirely in memory, and will instead be loaded in small chunks while it is being played.
If your sound can be loaded completely and can fit in memory, then audio streams won't help you: Just load the audio data into a sf::SoundBuffer
and use a regular sf::Sound
to play it.
sf::SoundStream
In order to define your own audio stream, you need to inherit from the sf::SoundStream
abstract base class.
There are two virtual functions to override in your derived class: onGetData
and onSeek
.
class MyAudioStream : public sf::SoundStream
{
bool onGetData(Chunk& data) override;
void onSeek(sf::Time timeOffset) override;
};
onGetData
is called by the base class whenever it runs out of audio samples and needs more of them.
You must provide new audio samples by filling the data
argument:
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;
}
You must return true
when everything is all right, or false
if playback must be stopped, either because an error has occurred or because there's simply no more audio data to play.
SFML makes an internal copy of the audio samples as soon as onGetData
returns, so you don't have to keep the original data alive if you don't want to.
The onSeek
function is called when the setPlayingOffset
public function is called.
Its purpose is to change the current playing position in the source data.
The parameter is a time value representing the new position from the beginning of the sound (not from the current position).
This function is sometimes impossible to implement.
In those cases leave it empty, and tell the users of your class that changing the playing position is not supported.
Now your class is almost ready to work.
The only thing that sf::SoundStream
needs to know now is the channel count, the sample rate of your stream, and a channel map, so that it can be played as expected.
To let the base class know about these parameters, you must call the initialize
protected function as soon as they are known in your stream class (which is most likely when the stream is loaded/initialized).
// where this is done totally depends on how your stream class is designed
unsigned int channelCount = ...;
unsigned int sampleRate = ...;
std::vector<SoundChannel> channelMap = ...;
initialize(channelCount, sampleRate, channelMap);
Threading issues
Audio streams are always played in a separate thread, therefore it is important to know what happens exactly, and where.
onSeek
is called directly by the setPlayingOffset
function, so it is always executed in the caller thread.
However, the onGetData
function will be called repeatedly as long as the stream is being played, in a separate thread created by SFML.
If your stream uses data that may be accessed concurrently in both the caller thread and in the playing thread, you have to protect it (with a mutex for example) in order to avoid concurrent access, which may cause undefined behavior -- corrupt data being played, crashes, etc.
Using your audio stream
Now that you have defined your own audio stream class, let's see how to use it.
In fact, things are very similar to what's shown in the tutorial about sf::Music
.
You can control playback with the play
, pause
, stop
and setPlayingOffset
functions.
You can also play with the sound's properties, such as the volume or the pitch.
You can refer to the API documentation or to the other audio tutorials for more details.
A simple example
Here is a very simple example of a custom audio stream class which plays the data of a sound buffer. Such a class might seem totally useless, but the point here is to focus on how the data is streamed by the class, regardless of where it comes from.
#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(),
{sf::SoundChannel::FrontLeft, sf::SoundChannel::FrontRight});
}
private:
bool onGetData(Chunk& data) override
{
// 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;
}
}
void onSeek(sf::Time timeOffset) override
{
// 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<std::int16_t> m_samples;
std::size_t m_currentSample{};
};
int main()
{
// load an audio buffer from a sound file
const sf::SoundBuffer buffer("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::Status::Playing)
sf::sleep(sf::seconds(0.1f));
}