Skip to content

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));
}