Skip to content

Custom audio streams#

Relevant example: echo

Audio stream? What's that?#

An audio stream is similar to music (remember the SF::Music class?). It has almost the same methods 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 methods to override in your derived class: on_get_data and on_seek.

class MyAudioStream < SF::SoundStream
  def on_get_data : Slice(Int16)?
  end

  def on_seek(time_offset : Time)
  end
end

on_get_data 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 returning a slice with data:

class MyAudioStream < SF::SoundStream
  def get_data
    samples_array.to_slice
  end
end

You must return a non-empty slice when everything is all right, or nil 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 on_get_data returns, so you don't have to keep the original data alive if you don't want to.

The on_seek method is called when the playing_offset= public method 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 method 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 and sample rate of your stream, so that it can be played as expected.

# where this is done totally depends on how your stream class is designed
MyAudioStream.new(channel_count: ..., sample_rate: ...)

Threading issues#

Audio streams are always played in a separate thread, therefore it is important to know what happens exactly, and where.

on_seek is called directly by the playing_offset= function, so it is always executed in the caller thread. However, the on_get_data 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.

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 SF::Music. You can control playback with the play, pause, stop and playing_offset= methods. 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.

require "crsfml/system"
require "crsfml/audio"

# custom audio stream that plays a loaded buffer
class MyStream < SF::SoundStream
  @samples : Slice(Int16)

  def initialize(@buffer : SF::SoundBuffer)
    # get a slice that points to the buffer with samples
    @samples = buffer.samples.to_slice(buffer.sample_count)

    # reset the current playing position
    @current_sample = 0

    # initialize the base class
    super(buffer.channel_count, buffer.sample_rate)
  end

  def on_get_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
    to_stream = {50000, @samples.size - @current_sample}.min
    # if the end is close, set the number to play the remaining samples

    # if nothing left to play
    return nil unless to_stream > 0

    # the next audio samples to be played
    result = (@samples.to_unsafe + @current_sample).to_slice(to_stream)

    # advance the position
    @current_sample += to_stream

    result
  end

  def on_seek(time_offset)
    # compute the corresponding sample index according to the
    # sample rate and channel count
    @current_sample = (
      time_offset.as_seconds *
      sample_rate * channel_count
    ).to_i
  end
end

# load an audio buffer from a sound file
buffer = SF::SoundBuffer.from_file("canary.wav")

# initialize and play our custom stream
stream = MyStream.new(buffer)
stream.play

# let it play until it is finished
while stream.status == SF::SoundSource::Playing
  SF.sleep(SF.seconds(1))
  # jump back every so often
  stream.playing_offset -= SF.seconds(0.7)
end