Skip to content

Composing and controlling

Michal Štrba edited this page Feb 26, 2019 · 38 revisions

In this part, we'll learn how to compose new, more complex streamers out of simpler ones and how to control their playback.

We'll start roughly where we left off in the previous part (excluding the resampling). If you don't have the code, here it is:

package main

import (
	"log"
	"os"
	"time"

	"github.com/faiface/beep"
	"github.com/faiface/beep/mp3"
	"github.com/faiface/beep/speaker"
)

func main() {
	f, err := os.Open("Rockafeller Skank.mp3")
	if err != nil {
		log.Fatal(err)
	}

	streamer, format, err := mp3.Decode(f)
	if err != nil {
		log.Fatal(err)
	}
	defer streamer.Close()

	speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))

	done := make(chan bool)
	speaker.Play(beep.Seq(streamer, beep.Callback(func() {
		done <- true
	})))

	<-done
}

Before we get into the hard-core composing and controlling, I'll teach you how you can observe a streamer, if it is a beep.StreamSeeker. Namely, how you can check it's current playing position.

A beep.StreamSeeker (which our streamer is) has three interesting methods: Len() int, Position() int, and Seek(p int) error. Note that all of the methods accept and return ints, not time.Duration. That's because a streamer itself doesn't know its sample rate, so all it can do is work with positions in the number of samples. We know the sample rate, though. We can use the format.SampleRate.D method to convert those ints to time.Duration. With this knowledge, we can easily track the current position of our streamer:

	done := make(chan bool)
	speaker.Play(beep.Seq(streamer, beep.Callback(func() {
		done <- true
	})))

	for {
		select {
		case <-done:
			return
		case <-time.After(time.Second):
			speaker.Lock()
			fmt.Println(format.SampleRate.D(streamer.Position()).Round(time.Second))
			speaker.Unlock()
		}
	}

We've replaced the simple <-done line with a more complex loop. It finishes when the playback finishes, but other than that, it prints the current streamer position every second.

Let's analyze how we do that. First, we lock the speaker with speaker.Lock(). Why is that? The speaker is pulling data from the streamer in the background, concurrently with the rest of the program. Locking the speaker temporarily prevents it from accessing all streamers. That way, we can safely access active streamers without running into race conditions.

After locking the speaker, we simply convert the streamer.Position() to a time.Duration, round it to seconds and print it out. Then we unlock the speaker so that the playback can continue.

Let's run it!

$ go run tracking.go
1s
2s
3s
4s
5s
6s
...

Works perfectly!

Now onto some composing and controlling!

Loop

Making new streamers from old ones, that's a common pattern in Beep. We've already seen it with beep.Resample and beep.Seq. The new streamer functions by using the old streamer under the hood, somehow manipulating its samples.

Another such streamer is beep.Loop. It's a very simple one. Keep all the code as is, just add the loop := ... line and edit speaker.Play to play the loop:

	loop := beep.Loop(3, streamer)

	done := make(chan bool)
	speaker.Play(beep.Seq(loop, beep.Callback(func() {
		done <- true
	})))

The beep.Loop function takes two arguments: the loop count, and a beep.StreamSeeker. It can't take just a regular beep.Streamer because it needs to rewind it for looping. Thankfully, the streamer is a beep.StreamSeeker.

Now, run the program and wait until the audio finishes:

$ go run loop.go
1s
2s
3s
...
3m33s
1s
2s
...

When the song finishes, it starts over. It will play 3x in total. If we set the loop count to negative, e.g. -1, it will loop indefinitely.

Maybe your song is too long and it takes too much to finish. No problemo! We can speed it up with beep.ResampleRatio (the first argument, 4, is once again the quality index):

	loop := beep.Loop(3, streamer)
	fast := beep.ResampleRatio(4, 5, loop)

	done := make(chan bool)
	speaker.Play(beep.Seq(fast, beep.Callback(func() {
		done <- true
	})))

This speeds up the playback 5x, so the output will look like this:

$ go run loop.go
6s
11s
16s
21s
...

Enjoy.

Notice one thing. We've wrapped the original streamer in beep.Loop, then we wrapped that in beep.ResampleRatio, but we're still getting and printing the current position directly from the original streamer! That's right. It nicely demonstrates how beep.Loop and beep.ResampleRatio use the original streamer, each time taking only as much data as they need.

Ctrl

Now we'll learn about another streamer. This time, we'll not only construct it, but we'll have dynamic control over it. Namely, we'll be able to pause and resume playback.

First, let's get rid of all the unnecessary code and start clean:

package main

import (
	"log"
	"os"
	"time"

	"github.com/faiface/beep/mp3"
	"github.com/faiface/beep/speaker"
)

func main() {
	f, err := os.Open("Crescendolls.mp3")
	if err != nil {
		log.Fatal(err)
	}

	streamer, format, err := mp3.Decode(f)
	if err != nil {
		log.Fatal(err)
	}
	defer streamer.Close()

	speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))

	// TODO
}

The goal is to enable the user to pause and resume the song by entering a newline. Pretty simple user interface. First, let's play the song on the loop. But, we'll additionally wrap it in beep.Ctrl, which will enable us to pause the playback:

	speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))

	ctrl := &beep.Ctrl{Streamer: beep.Loop(-1, streamer), Paused: false}
	speaker.Play(ctrl)

This is a little different. All previous streamers were constructed using some constructor function, beep.Seq, beep.Resample, etc., but all we do here is directly make a struct. As you can see, the struct has two fields: the wrapped streamer, and a Paused flag. Here's how the Ctrl streamer works: when Paused is false, it streams from the wrapped streamer; when it's true, it streams silence.

Also, this time we don't need to hang until the song finishes, because we'll instead be handling user input in an infinite loop. Let's do that:

	speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))

	ctrl := &beep.Ctrl{Streamer: beep.Loop(-1, streamer), Paused: false}
	speaker.Play(ctrl)

	for {
		fmt.Print("Press [ENTER] to pause/resume. ")
		fmt.Scanln()

		// TODO: pause/resume
	}

That's the structure of the loop. Prompt the user, get the newline. All that's missing is actually pausing and resuming playback.

To do that, all we have to do is to switch the Paused flag in ctrl. Well, almost all. We also need to lock the speaker, because we are accessing an active streamer.

	for {
		fmt.Print("Press [ENTER] to pause/resume. ")
		fmt.Scanln()

		speaker.Lock()
		ctrl.Paused = !ctrl.Paused
		speaker.Unlock()
	}

And that's it!

Volume

We'll take a look at one more streamer, this time not from the beep package, but from the extra "github.com/faiface/beep/effects" package. The effect we'll be dealing with is simple: changing volume.

What's the deal with volume? It sounds like an easy thing, but the tricky part is that human volume perception is roughly logarithmic. What it means is that two cars will not be 2x louder that one car, even though the intensity of the signal will double. In fact, that would be silly. Imagine a highway being 100x louder than a single car. Two cars will be, in our perception, lounder than a single car only by a constant. Doubling the number of cars once more will, again, increase the volume by the same constant. You see what's going on: to get a perception of increasing the volume by a constant, we need to multiply the intensity of the signal.

TODO