Skip to content

Making own streamers

Michal Štrba edited this page Feb 28, 2019 · 15 revisions

Beep offers a lot of pre-made streamers, but sometimes that's not enough. Fortunately, making a new streamer isn't very hard and in this part, we'll learn just that.

So, what's a streamer? It's this interface:

type Streamer interface {
    Stream(samples [][2]float64) (n int, ok bool)
    Err() error
}

Read the docs for more details.

Why does Stream return a bool and error handling is moved to a separate Err method? The main reason is to prevent one faulty streamer from ruining your whole audio pipeline, yet make it possible to catch the error and handle it somehow.

How would a single faulty streamer ruin your whole pipeline? For example, there's a streamer called beep.Mixer, which mixes multiple streamers together and makes it possible to add streamers dynamically to it. The speaker package uses beep.Mixer under the hood. The mixer works by gathering samples from all of the streamers added to it and adding those together. If the Stream method returned an error, what should the mixer's Stream method return if one of its streamers errored? There'd be two choices: either it returns the error but halts its own playback, or it doesn't return it, thereby making it inaccessible. Neither choice is good and that's why the Streamer interface is designed as it is.

To make our very own streamer, all that's needed is implementing that interface. Let's get to it!

Noise generator

This will probably be the simplest streamer ever. It'll stream completely random samples, resulting in a noise. To implement an interface, we need to make a type. The noise generator requires no state, so it'll be an empty struct:

type Noise struct{}

Now we need to implement the Stream method. It receives a slice and it should fill it will samples. Then it should return how many samples it filled and a bool depending on whether it was already drained or not. The noise generator will stream forever, so it will always fully fill the slice and return true.

The samples are expected to be values between -1 and +1 (including). We fill the slice using a simple for-loop:

func (n Noise) Stream(samples [][2]float64) (n int, ok bool) {
	for i := range samples {
		samples[i][0] = rand.Float64()*2 - 1
		samples[i][1] = rand.Float64()*2 - 1
	}
	return len(samples), true
}

The last thing remaining is the Err method. The noise generator can never malfunction, so Err always returns nil:

func (n Noise) Err() error {
	return nil
}

Now it's done and we can use it in a program:

func main() {
	sr := beep.SampleRate(44100)
	speaker.Init(sr, sr.N(time.Second/10))
	speaker.Play(Noise{})
	select {}
}

This will play noise indefinitely. Or, if we only want to play it for a certain period of time, we can use beep.Take:

func main() {
	sr := beep.SampleRate(44100)
	speaker.Init(sr, sr.N(time.Second/10))

	done := make(chan bool)
	speaker.Play(beep.Seq(beep.Take(sr.N(5*time.Second), Noise{}), beep.Callback(func() {
		done <- true
	})))
	<-done
}

This will play noise for 5 seconds.

Since streamers that never fail are fairly common, Beep provides a helper type called beep.StreamerFunc, which is defined like this:

type StreamerFunc func(samples [][2]float64) (n int, ok bool)

It implements the Streamer interface by calling itself from the Stream method and always returning nil from the Err method. As you can surely see, we can simplify our Noise streamer definition by getting rid of the custom type and using beep.StreamerFunc instead:

func Noise() beep.Streamer {
	return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) {
		for i := range samples {
			samples[i][0] = rand.Float64()*2 - 1
			samples[i][1] = rand.Float64()*2 - 1
		}
		return len(samples), true
	})
}

We've changed the streamer from a struct to a function, so we need to replace all Noise{} with Noise(), but other than that, everything will be the same.

Queue

Well, that was simple. How about something more complex?