-
Notifications
You must be signed in to change notification settings - Fork 17
Making own streamers
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 abool
and error handling is moved to a separateErr
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. Thespeaker
package usesbeep.Mixer
under the hood. The mixer works by gathering samples from all of the streamers added to it and adding those together. If theStream
method returned an error, what should the mixer'sStream
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 theStreamer
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!
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 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.
Well, that was simple. How about something more complex?