How I built an audio library using the composite pattern and higher-order functions
Some people say that Go can’t express good abstractions. They mostly refer to the lack of generics. That’s because in most object-oriented languages, people are used to creating abstractions around types. In Go, the right way is to create abstractions around behaviour using interfaces. When you follow this principle, you find that Go is very powerful at creating abstractions.
In this post, I am going to explore a way of creating abstractions using the ‘good old composite pattern’ from the book called ‘Design patterns’. Combined with higher-order functions, this pattern is not only powerful, but also convenient. I will
- Explain what the composite pattern is.
- Show how it can be combined with higher-order functions and closures.
- Present a way I used it to create an audio library.
Why the hell are you going to talk about design patterns? Those are for Java and Java is bad, we don’t like design patterns in Go!
Uhm, well, you’re right, we usually don’t like design patterns in Go. But, bear with me, this is only going to be about one of them and it’s gonna be interesting, I promise.
First example: arithmetic expressions
The composite pattern revolves around a single idea:
Everything satisfies a single interface and complex things are composed from simple ones.
It might not be entirely clear what this means, so let’s dive directly into the code.
In this first example, we’ll be creating a system to create and evaluate arithmetic expressions. We need to start by creating the one single interface. It’s clear that an interface representing an expression will be the right choice. This way, we’ll be able to create simple expressions and create more complicated expressions by composing simpler ones using various operators.
To make things even simpler, we won’t even define an interface. A function type is sufficient here.
type Expr func() float64
So, an expression is a function which evaluates to float64. An interface equivalent would be an
interface with a single method, say Eval.
The most simple expression is one that returns a constant. Let’s call it Const. Here’s the
constructor.
func Const(x float64) Expr {
return func() float64 {
return x
}
}
Now we can define a composite constructor, say a one which sums other expressions. It takes an aribirary number of other expressions and evaluates to their sum.
func Sum(exs ...Expr) Expr {
return func() float64 {
sum := 0.0
for _, ex := range exs {
sum += ex()
}
return sum
}
}
The expression returned from the Sum constructor simply adds the results of the other expressions
together. Note, that we call ex() inside the loop, because the Expr type is a function and
calling it yields the result.
Now we have some material to start using our arithmetic expressions library.
ex1 := Sum(Const(1), Const(2), Const(3))
fmt.Println(ex1()) // prints 6
ex2 := Sum(Const(1), Sum(Const(2), Const(3)))
fmt.Println(ex2()) // prints 6 alike
We can create more expressions. Ref always returns the value of a variable it is pointing to.
func Ref(x *float64) Expr {
return func() float64 {
return *x
}
}
And now we can do funny things :)
x := 1.0
ex := Sum(Ref(&x), Ref(&x))
x = ex() // x = 2
x = ex() // x = 4
x = ex() // x = 8
x = ex() // x = 16
I think this is neat. This is just a little toy example, to give you the taste. Let’s go ahead to another example. Still a toy one, but a little less toy one.
Second example: sequences
Now, we will use the composite pattern to assemble sequences of numbers. Finite or inifinite, doesn’t matter.
Once again, we need to start by creating the single interface. Obviously, this interface will
represent a sequence. The interface will contain a single method called Next. This method returns
the next element in the sequence and true or false depending on whether we already reached the
end of the sequence.
type Seq interface {
Next() (x float64, ok bool)
}
We could use a function type again, but I decided to use a proper interface, for clarity. Nonetheless, we can create a helper function type, which will simplify creating instances of the interface.
type SeqFunc func() (x float64, ok bool)
func (sf SeqFunc) Next() (x float64, ok bool) {
return sf()
}
The SeqFunc is a function type which satisfies the Seq interface by implementing the Next
function returning the result of itself. This pattern is also used in the standard library with
http.HandlerFunc.
Let’s start creating actual Seq implementations.
The simplest one is a sequence created from a slice. We’ll call it Mk (make a sequence).
func Mk(vals ...float64) Seq {
i := 0
return SeqFunc(func() (x float64, ok bool) {
if i >= len(vals) {
return 0, false
}
x = vals[i]
i++
return x, true
})
}
Mk sequence tracks the current index in the slice it was created from. Every time Next is
called, the current value is returned and the index is incremented. When we reach the end of the
slice, we simply return 0, false.
A complement function to Mk is Collect, which takes a Seq and collects it into a slice of
numbers. This is obviously only applicable to finite sequences.
func Collect(s Seq) []float64 {
var vals []float64
for {
x, ok := s.Next()
if !ok {
break
}
vals = append(vals, x)
}
return vals
}
Now, we can turn a slice into a sequence and back again, very useful.
fmt.Println(Collect(Mk(1, 2, 3))) // prints [1 2 3]
We can create an infinite sequence of natural numbers.
func Naturals() Seq {
n := 0.0
return SeqFunc(func() (x float64, ok bool) {
n++
return n, true
})
}
And we can create a composite which takes n first elements out of any sequence.
func Take(n int, s Seq) Seq {
return SeqFunc(func() (x float64, ok bool) {
if n <= 0 {
return 0, false
}
n--
return s.Next()
})
}
Now, we can generate n first natural numbers.
fmt.Println(Collect(Take(10, Naturals())))
// [1 2 3 4 5 6 7 8 9 10]
This is starting to feel like functional programming. And rightfully so. We are doing
- Composition
- Lazy evaluation and infinite sequences
- Too long expressions :)
The first point is the most important. With the composite pattern, we’ve been able to create
building blocks and compose more complicated things out of them. Don’t believe me? Let’s create a
Split function, which takes a sequence and splits it into two sequences at a given index. Just
watch!
func Split(n int, s Seq) (l, r Seq) {
return Mk(Collect(Take(n, s))...), s
}
That was easy! Make sure to understand it, should be fairly obvious.
To make the Split function, we didn’t have to write almost any code. All of the required pieces
were already present, we just needed to compose them in the right way. Hence, the composite pattern.
Let’s see how our Split function behaves.
s := Take(7, Naturals())
l, r := Split(4, s)
fmt.Println(Collect(l), Collect(r))
// [1 2 3 4] [5 6 7]
Take seven naturals, split them at four and print the results. Simple, right?
When using the composite pattern, you’ll find it very easy to follow the DRY principle and you usually end up writing much less code than otherwise.
Excercise: Create a Dup (duplicate) function, which takes a Seq and returns two Seqs which
are both identical (in terms of elements) to the original Seq. It should work for infinite
sequences too.
Real thing: Beep audio library
Now that you’ve certainly got a hang of the composite pattern, let’s see how it can be used in a real library. The library I’m talking about is an audio library I’ve been working on for the past month. It’s called Beep, which is a name chosen from a poll on Reddit.
First things first, the library is by no means only my work. The playback heavily depends on Hajime Hoshi’s Oto library and MP3 decoding depends on his go-mp3 package.
The library is built around the composite pattern, for a great good. Let’s take a look!
The Streamer interface
As usual, when designing around the composite pattern, the first thing is the single interface. In
case of Beep, this single interface is called Streamer. It’s very similar to the former one,
Seq.
Audio is composed of elements called
samples. A sample is a number on the
timeline in the interval [-1, +1] which specifies the pressure of the air at that moment. A
Streamer is pretty much a sequence of samples. However, getting the samples one by one as with
Seq would be way too inefficient. That’s why the Streamer interface is designed to stream a
number of samples at once.
Here’s how it looks like.
type Streamer interface {
Stream(samples [][2]float64) (n int, ok bool)
Err() error
}
The Stream method is given a slice of samples (there are two numbers per sample, the left and the
right channel), fills it with samples and advances the internal state (if any). It returns the
number of samples it streamed (filled in the slice). If the Streamer already reached it’s end and
has no samples to stream, it returns false. Otherwise, it returns true. The complete description
can be found in the docs.
The Err method is for handling errors, such as file and network errors. Errors are kept out of the
Stream method to simplify composition.
Similar to Seq, there is a helper function type which enables easy creation of Streamers that
can’t error.
type StreamerFunc func(samples [][2]float64) (n int, ok bool)
Loading audio files
Enough talking, let’s get to the code.
The simplest way to create a streamer is from an audio file. Beep currently supports WAV and MP3 formats. We’ll be ignoring errors here, for simplicity, don’t do that in real code.
// import "github.com/faiface/beep/mp3"
file, _ := os.Open("song.mp3")
streamer, format, _ := mp3.Decode(file)
This creates a streamer which streams the song directly from the file without loading it in the memory first. The song doesn’t even have to fit in the memory, since it’s never fully there.
The format return value tells us some information about the source file, such as the sample
rate (number of samples
per second) and the number of channels.
Let’s go a head and convert the MP3 file into a WAV file. We’ve already created a streamer for the MP3 file, so all we need to do is to encode this streamer into a WAV file.
// import "github.com/faiface/beep/wav"
output, _ := os.Create("song.wav")
wav.Encode(output, streamer, format)
output.Close()
This is quite spectacular. There is no MP3 to WAV conversion functionality in the library
anywhere. What is there is an MP3 to Streamer functionality and Streamer to WAV functionality.
Composing these two things yields an MP3 to WAV converter. Furthermore, this converter operates
directly on the files, doesn’t load the files into the memory. You can convert gigabytes of audio
data without ruining your RAM. All of this simply follows from the chosen composite interface and
its composition.
Playing through the speaker
Loading and saving audio files is interesting, but playing is the key. Beep has an extension package
"github.com/faiface/beep/speaker", which allows playback of arbitrary streamers.
First, we need to initialize the speaker, which involves setting the speaker’s sample rate and
buffer size. This is done using the Init function.
// import (
// "github.com/faiface/beep"
// "github.com/faiface/beep/speaker"
// )
sr := beep.SampleRate(48000)
speaker.Init(sr, sr.N(time.Second/10))
The type beep.SampleRate is an int with
two methods. First, sr.D, converts number of samples to their duration in time.Duration. The
second, sr.N, does the opposite. Since speaker.Init takes the number of samples in the buffer,
we use the sr.N method to calculate the number of samples in 1/10s. Smaller buffer size means more
responsive playback (lower latency). Larger buffer size means more reliable playback.
To play a streamer, we simply call the speaker.Play function.
speaker.Play(streamer)
This starts the playback and returns immediately.
Following this, here’s how to play an MP3 file.
file, _ := os.Open("song.mp3")
streamer, format, _ := mp3.Decode(file)
sr := format.SampleRate
speaker.Init(sr, sr.N(time.Second/10))
speaker.Play(streamer)
Nice! There is a problem with this code, though. This program immediately exits and we won’t hear
anything. The solution would be to either wait infinitely using select {}, or somehow wait until
the song ends. We’ll learn about that in the following section.
Composing Streamers
Beep provides a number of built-in streamer compositors. Let’s take a look at a few of them.
One of the most important ones is Seq. Don’t get confused, this has nothing to do with the
sequence composite interface above. It takes some streamers and streams them one after another.
Here’s how it’s implemented.
func Seq(s ...Streamer) Streamer {
i := 0
return StreamerFunc(func(samples [][2]float64) (n int, ok bool) {
for i < len(s) && len(samples) > 0 {
sn, sok := s[i].Stream(samples)
samples = samples[sn:]
n, ok = n+sn, ok || sok
if !sok {
i++
}
}
return n, ok
})
}
Pretty simple. I’m not going to explain it in detail, you can do that yourself if you want. What’s more important is how we can use this compositor.
speaker.Play(beep.Seq(streamer1, streamer2))
This code plays two different streamers one after another. Note, that a streamer is not immutable
and gets drained after playing. Therefore beep.Seq(streamer1, streamer1) will not play the same
streamer twice, because on the second time it will already be drained.
Another useful streamer is Callback. This is a streamer which streams no audio, but instead calls
a function when it starts streaming (and ends streaming immediately).
With this knowledge, we are ready to play a song until it ends.
done := make(chan struct{})
speaker.Play(beep.Seq(
streamer,
beep.Callback(func() {
close(done)
})
))
<-done
We create a channel which will signal the end of the song. Then, we play a sequence of the song and a callback. Therefore, when the song finishes, the callback gets called. The callback closes the signaling channel and causes the receive operation proceed. Neat.
What if we want to play a streamer which streams at a different sample rate than the speaker? If
you’d just play it as it is, the playback would be either too fast or too slow. Beep provides a
Resample compositor exactly for this.
Let’s say we have a streamer which streams at the sample rate of 44100, but we initialized the
speaker to the sample rate of 48000. Here’s how we do it.
speaker.Play(beep.Resample(4, 44100, 48000, streamer))
The first argument is the resampling quality. Read more about it in the docs.
Resampling can also be used to speed up or slow down an audio.
twiceAsFast := beep.ResampleRatio(4, 2, streamer)
speaker.Play(twiceAsFast)
Beep provides a variety of other compositor. Let’s list a few.
playTogether := beep.Mix(streamer1, streamer2)
firstFiveSeconds := beep.Take(sr.N(5*time.Second), streamer3)
ctrl := beep.Ctrl{Streamer: streamer4}
ctrl.Paused = true
minuteOfSilence := beep.Silence(sr.N(time.Minute))
louder := effects.Gain{
Streamer: streamer5,
Gain: 2,
}
toTheLeft := effects.Pan{
Streamer: streamer6,
Pan: -1,
}
Of course, if the built-in compositors and streamers are not enugh, you can easily create your own
ones. All you need to do is to implement a Streamer. For example, here’s an infinite sine wave
streamer. Of course, you can use beep.Take on it to turn it into a finite streamer.
func SineWave(sr beep.SampleRate, freq float64) beep.Streamer {
t := 0.0
return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) {
for i := range samples {
y := math.Sin(math.Pi * freq * t)
samples[i][0] = y
samples[i][1] = y
t += sr.D(1).Seconds()
}
return len(samples), true
})
}
Conclusion
This article showed what the composite pattern is, how it can be combined with higher-order functions and closures for convenience and how you can use it to create flexible and reusable code.
Of course, the composite pattern is not a fit for everything. But when it is, please consider using it, as it will greatly improve the quality and DRYness of your code.
I think it’s a great fit for Go and an idiomatic solution to many abstraction problems. Go has interfaces and higher-order functions, so use them!
Currently, I’m figuring out how to incorporate the composite pattern into my Pixel game library. If you have any ideas on that, please share ;)
Michal Štrba
18 archived comments
Posted on the previous version of this blog. They're read-only now; new comments go below.
Great article, thanks!
I was wondering, why in Split do you do
> return Mk(Collect(Take(n, s))...), s
instead of just:
> return Take(n, s), s
I realize they have slightly different behavior as you can see here https://play.golang.org/p/5.... Still, it seems if you're using strictly functional patterns those behaviors wouldn't come about, so why the extra calls to Collect and Mk?
Would also love to see your solution to the Dup exercise.
The main difference between Mk(Collect(Take(...))) and Take(...) is apparent from here: https://play.golang.org/p/s...
The thing to realize is that Seq is not immutable. Calling Next will advance the sequence, thus mutate it. That's why we need to collect the first n elements from the sequence, or otherwise we couldn't use the split parts independently. Hope this answers your question.
Regarding the Dup exercise, it's not nearly as nice as Split :). You have to "buffer" (remember / cache) the elements from the returned sequence which is currently ahead of the second returned sequence and vice-versa, if the other one gets ahead. The sequence which is behind will return those cached elements. You need to make sure, that old cached elements get garbage collected, but that's easy, if you work properly with slices.
You can take a look at how I implemented Dup for Beep here: https://github.com/faiface/...
I was browsing my own gists many years later (today) and came across my attempt at the Dup Exercise, and looking at your implementation, it seems we did it in a similar way.
func Dup: https://gist.github.com/mar...
using a bufferedSeq: https://gist.github.com/mar...
Nice to know someone still coming here. :D Good job on the exercise, too!
This is mind-boggling a bit. Let me try and clear this for myself. The composites (Take for example) don't get "executed" until they get passed to Collect, right? Up until that point they're like stored patterns with limiting values (except for Naturals). Is this explanation in the ball park of what's actually happening? :)
Yep, that sounds correct. Seq doesn't get "executed" until you call Next and Collect calls Next until the end of the sequence. This is actually called lazy evaluation, which means, that we never compute ahead-of-time, we always only compute as much as we need (Next only computes one element). This is a common practice in functional programming.
Ah, I see. Thank you for clarifying. :)
Brilliant idea. Do you have an email list? I would like to read future posts without having to check every now and then.
Just got into Go and have to ask: can you please elaborate just a bit on the "we usually don’t like design patterns in Go" part? While it is clear that go's OOP is a different from what you may see in, say, Ruby - why is do 'we' don't like design patterns in Go?
Yeah, don't read too much into that line. All I wanted to say is that Go's approach to OOP is a little different than say Java or C++ and thus design patterns often don't apply or are replaced by other concepts (Strategy can be usually replaced by closures, etc.)
Hi Michal,
Thanks for the great post and package "beep". I am try to use your package to my project. Just one question, I need a 16bits 8Khz wave format for my project. is there a way to convert the mp3 decoded raw 44Khz samples to 8Khz, it likes the sox did in this example https://sourceforge.net/p/s.... I tried the package resample, it just low down the speed of play :(. any suggestion?
Thank you
You need to play it/save it at the new rate too, in order for it to sound the same. When you play fewer/more samples in the same rate as before, it's going to be faster/slower.
Thanks for you quick reply :).
I added few code to mp3-playing.go example
----------
sr := beep.SampleRate(8000)
speaker.Init(sr, sr.N(time.Second/2))
speaker.Play(beep.Resample(3, format.SampleRate, sr, s))
---------
It quit immediately, How I can let it continue to play :), sorry I am newbie for Go :)
The same way you avoid immediately exiting as with any other beep.Streamer. Either select{} at the end, or sleep an appropriate time, or, the best, sequence it with the "exit" callback as described in the Beep's README.
great, I checked the README again, I got it works, thanks again :)
Hi Michal,
I wrote few lines for how my code to use the beep as below shows
---------
package dataprocessor
import (
"errors"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep"
"net/http"
log "github.com/Sirupsen/logrus"
"sync"
)
type Dataprocessor struct {
s beep.Streamer
rate int
}
var (
samples [][2]float64
buf []byte
mu sync.Mutex
)
func init() {
//TODO initial buffer
}
func New(rate int) (*Dataprocessor, error) {
resp, err := http.Get("http://newcap.leanstream.co...")
if err != nil {
log.Fatal(err)
}
s, format, _ := mp3.Decode(resp.Body)
log.Println("mp3 info", format.NumChannels, format.SampleRate, format.Precision)
sr := beep.SampleRate(rate)
sn := beep.Seq(beep.Resample(3, format.SampleRate, sr, s))
numBytes := 4000 * 4
samples = make([][2]float64, 4000)
buf = make([]byte, numBytes)
p := Dataprocessor{
s: sn,
rate: rate,
}
return &p, nil
}
func (d *Dataprocessor) ProcessRate() (int, error) {
sr, ok := d.s.Stream(samples)
if !ok {
return 0, errors.New("read out samples from streamer is wrong")
}
return sr, nil
}
func (d *Dataprocessor) Read() ([]byte, int, error){
mu.Lock()
cnt, err := d.ProcessRate()
if err != nil {
return nil, cnt, errors.New("read out samples from streamer is wrong")
}
for i := range samples {
for c := range samples[i] {
//take one channel
//var c = 1
val := samples[i][c]
if val < -1 {
val = -1
}
if val > +1 {
val = +1
}
valInt16 := int16(val * (1<<15 - 1))
low := byte(valInt16)
high := byte(valInt16 >> 8)
buf[i*4+c*2+0] = low
buf[i*4+c*2+1] = high
}
}
mu.Unlock()
log.Println("resamples is", len(buf))
return buf, len(buf), nil
}
--------------
basically, my code is call the Read function to get the decoded and rate changed samples, but it always crashed on resample.go line 96 like below shows
------------
panic: runtime error: index out of range
goroutine 35 [running]:
github.com/faiface/beep.(*R..., 0xc4201c0000, 0x8bd, 0x8bd, 0xc420053808, 0x52cd0d)
/root/work/src/github.com/faiface/beep/res... +0x465
github.com/faiface/beep.Seq..., 0xfa0, 0xfa0, 0xc400000000, 0x458de0)
/root/work/src/github.com/faiface/beep/com... +0xc6
github.com/faiface/beep.Str..., 0xc4201c0000, 0xfa0, 0xfa0, 0xc4200538a8, 0x430e1e)
/root/work/src/github.com/faiface/beep/int... +0x44
------------
what is wrong in there, would you please look at that? thank you
I stumbled onto this article yesterday and spent the rest of the day working through your repo. It's really fantastic. Thanks for sharing.
I'm really interested in doing something similar so I have to ask, can you recommend any resources for building audio software? Or did you build this based more on intuition of how an audio engine would work? Thanks again.
Hi! Glad you liked it :)
Before I build Beep, I was researching and looking how other libraries design their API, most notably OpenAL and SFML in C++ (SFML uses OpenAL). Now, this might sound pretentious, but I found OpenAL quite terrible. It has these buffers that are supposed to make it more performant, but in reality they increase latency and make it very hard to apply audio effects in real time, or stream music in real time.
So, I though I could do better. Audio streaming and processing is not very CPU costly, so why make a complicated framework when you can just stream audio samples directly to the output, right? So that's what I did and as I was designing it, I figured out all these little ways one can compose things together and ended up with this pretty flexible, and compared to OpenAL, almost latency-free library.
Regarding the design of Beep, yeah, that's entirely mine.
But to answer your question: I recommend learning about ALSA, a very low level, but very simple library for directly handling audio hardware. You can check out OpenAL, but it's more complicated than ALSA and also less flexible. Many libraries, including SFML are built on OpenAL. Their API is not bad, so you can check that out too ;)