View on GitHub

DSFML

Custom audio streams

Audio stream? What's that?

An audio stream is similar to music (remember the 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 DSFML doesn't support, etc.

In fact, the 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 SoundBuffer and use a regular Sound to play it.

SoundStream

In order to define your own audio stream, you need to inherit from the SoundStream abstract base class. There are two virtual functions to override in your derived class: onGetData() and onSeek().

class MyAudioStream : SoundStream
{
    override bool onGetData(ref const(short)[] data);

    override void onSeek(Time timeOffset);
}

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 onGetData(ref const(short)[] data)
{
    data.samples = /* fill the array with new audio samples */;
    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.

DSFML 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 SoundStream needs to know now is the channel count and sample rate of your stream, 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
uint channelCount = ...;
uint sampleRate = ...;
initialize(channelCount, sampleRate);

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 DSFML. 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.

If you're not familiar enough with threading, you can refer to the corresponding tutorial for more information.

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 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.

import dsfml.audio;

// custom audio stream that plays a loaded buffer
class MyStream : SoundStream
{
    private
    {
        short[] m_samples;
        size_t m_currentSample;
    }

    void load(const(SoundBuffer) buffer)
    {
        // extract the audio samples from the sound buffer to our own container
        m_samples = buffer.getSamples().dup;

        // reset the current playing position
        m_currentSample = 0;

        // initialize the base class
        initialize(buffer.getChannelCount(), buffer.getSampleRate());
    }

private:

    override bool onGetData(ref const(short)[] 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;

        // have we reached the end of the sound?
        if (m_currentSample + samplesToStream <= m_samples.length)
        {
            // end not reached: stream the samples and continue
            data = m_samples[m_currentSample .. m_currentSample + samplesToStream];
            m_currentSample += samplesToStream;
            return true;
        }
        else
        {
            // end of stream reached: stream the remaining samples and stop playback
            data = m_samples[m_currentSample .. $];
            m_currentSample = m_samples.length;
            return false;
        }
    }

    override void onSeek(Time timeOffset)
    {
        // compute the corresponding sample index according to the sample rate and channel count
        m_currentSample = timeOffset.asSeconds() * getSampleRate() * getChannelCount();
    }
}

int main()
{
    // load an audio buffer from a sound file
    SoundBuffer buffer = new SoundBuffer();
    buffer.loadFromFile("sound.wav");

    // initialize and play our custom stream
    MyStream stream = new MyStream();
    stream.load(buffer);
    stream.play();

    // let it play until it is finished
    while (stream.getStatus() == SoundSource.Status.Playing)
        sleep(seconds(0.1f));

    return 0;
}