From 86a0dfb364ea8c3778a2cc7044c26419aba4d2fa Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Sun, 20 Oct 2024 21:50:24 +0200 Subject: [PATCH 1/3] Add docs from wiki --- docs/Composing-and-controlling.md | 315 ++++++++++++++++++ docs/Hello,-Beep!.md | 197 +++++++++++ docs/Home.md | 11 + docs/Making-own-streamers.md | 211 ++++++++++++ ...-or-not-to-buffer,-that-is-the-question.md | 102 ++++++ docs/_Sidebar.md | 6 + 6 files changed, 842 insertions(+) create mode 100644 docs/Composing-and-controlling.md create mode 100644 docs/Hello,-Beep!.md create mode 100644 docs/Home.md create mode 100644 docs/Making-own-streamers.md create mode 100644 docs/To-buffer,-or-not-to-buffer,-that-is-the-question.md create mode 100644 docs/_Sidebar.md diff --git a/docs/Composing-and-controlling.md b/docs/Composing-and-controlling.md new file mode 100644 index 0000000..703227d --- /dev/null +++ b/docs/Composing-and-controlling.md @@ -0,0 +1,315 @@ +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: + +```go +package main + +import ( + "log" + "os" + "time" + + "github.com/gopxl/beep" + "github.com/gopxl/beep/mp3" + "github.com/gopxl/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`](https://godoc.org/github.com/gopxl/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 those methods accept and return `int`s, not `time.Duration`. That's because a streamer itself doesn't know its sample rate, so all it can do is work with the numbers of samples. We know the sample rate, though. We can use the `format.SampleRate.D` method to convert those `int`s to `time.Duration`. This way, we can easily track the current position of our streamer: + +```go + 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`](https://godoc.org/github.com/gopxl/beep#Resample) and [`beep.Seq`](https://godoc.org/github.com/gopxl/beep#Seq). The new streamer functions by using the old streamer under the hood, somehow manipulating its samples. + +Another such streamer is [`beep.Loop`](https://godoc.org/github.com/gopxl/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`: + +```go + 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`](https://godoc.org/github.com/gopxl/beep#StreamSeeker). It can't take just a regular [`beep.Streamer`](https://godoc.org/github.com/gopxl/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`](https://godoc.org/github.com/gopxl/beep#ResampleRatio) (the first argument, `4`, is once again the quality index): + +```go + 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: + +```go +package main + +import ( + "log" + "os" + "time" + + "github.com/gopxl/beep/mp3" + "github.com/gopxl/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 a `beep.Ctrl`, which will enable us to pause the playback: + +```go + 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. + +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: + +```go + 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 the 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. + +```go + 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/gopxl/beep/effects"`](https://godoc.org/github.com/gopxl/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, louder 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 increase the perception of volume by a constant, we need to multiply the intensity of the signal_. + +The [`effects.Volume`](https://godoc.org/github.com/gopxl/beep/effects#Volume) streamer is a struct, just like `beep.Ctrl`, and looks like this: + +```go +type Volume struct { + Streamer beep.Streamer + Base float64 + Volume float64 + Silent bool +} +``` + +Four fields here. The first one is obvious. The second one means this: _to increase the `Volume` by 1 means to multiply the signal (samples) by `Base`_. The `Volume` of 0 means unchanged volume, going into the positive numbers increases the volume, and going into the negative ones decreases it. For example, the `Volume` of -1 divides the signal by `Base`. In general, the signal will be multiplied by `math.Pow(Base, Volume)`. Since there's no `Volume` value that would completely silence the audio, an additional `Silent` flag is needed. Setting it to `true` will not pause the playback but will make it silent. + +> **What's a good `Base` value?** If you've heard about [Decibels](https://en.wikipedia.org/wiki/Decibel), you might be tempted to say that 10 is the right value. From my experience, that is not true at all. The most natural `Base` values I've found were somewhere around 2. + +Now, pausing is bad, right? It ruins the experience! So, we'll punish the user for pausing. Whenever the user pauses, we will increase the volume. Sounds fun? Let's do it! + +First, we will wrap the `ctrl` streamer in `effects.Volume` (we can't do it the other way around, because `effects.Volume` is not a `beep.StreamSeeker`): + +```go + ctrl := &beep.Ctrl{Streamer: beep.Loop(-1, streamer), Paused: false} + volume := &effects.Volume{ + Streamer: ctrl, + Base: 2, + Volume: 0, + Silent: false, + } + speaker.Play(volume) +``` + +Once again, `Volume` of 0 means _unchanged_ volume, not muted. + +Now, let's increase the volume whenever the user pauses or resumes playback: + +```go + for { + fmt.Print("Press [ENTER] to pause/resume. ") + fmt.Scanln() + + speaker.Lock() + ctrl.Paused = !ctrl.Paused + volume.Volume += 0.5 // <-- this right here + speaker.Unlock() + } +``` + +Done! + +But well, just increasing the volume might not be enough a punishment. Let's also speed up the playback, that'll show them! So, we'll additionally wrap with [`beep.ResampleRatio`](https://godoc.org/github.com/gopxl/beep#ResampleRatio): + +```go + ctrl := &beep.Ctrl{Streamer: beep.Loop(-1, streamer), Paused: false} + volume := &effects.Volume{ + Streamer: ctrl, + Base: 2, + Volume: 0, + Silent: false, + } + speedy := beep.ResampleRatio(4, 1, volume) + speaker.Play(speedy) +``` + +We're starting with the resampling ratio of 1: no change in speed. But, we'll increase the ratio whenever the user pauses or resumes: + +```go + for { + fmt.Print("Press [ENTER] to pause/resume. ") + fmt.Scanln() + + speaker.Lock() + ctrl.Paused = !ctrl.Paused + volume.Volume += 0.5 + speedy.SetRatio(speedy.Ratio() + 0.1) // <-- right here + speaker.Unlock() + } +``` + +And that's it! This will guarantee that no one will use the pause/resume feature ever again! + +## Conclusion + +The main takeaway from this part is that while each streamer only does a specific thing (`beep.Ctrl` pauses, `beep.Volume` changes volume, etc.) you can easily combine them together and create your own, powerful _"DJ panels"_. This flexibility and modularity allows for easily extending Beep with new effects and being able to seamlessly integrate them into existing code without any restructuring. This is, in my opinion, the biggest benefit of Beep's design. + +Look into the documentation of both [`beep`](https://godoc.org/github.com/gopxl/beep) and [`effects`](https://godoc.org/github.com/gopxl/beep/effects) package to find other useful streamers. \ No newline at end of file diff --git a/docs/Hello,-Beep!.md b/docs/Hello,-Beep!.md new file mode 100644 index 0000000..6f4e8f1 --- /dev/null +++ b/docs/Hello,-Beep!.md @@ -0,0 +1,197 @@ +Welcome to the Beep tutorial! In this part, we'll learn how to load a song, initialize the speaker, and wake up your neighbors. + +The first thing we obviously need is the [Beep library](https://github.com/gopxl/beep/) (I expect you have the Go programming language installed), which you can install with this command: + +``` +$ go get -u github.com/gopxl/beep +``` + +We'll start with a plain main function and we'll import the Beep package: + +```go +package main + +import "github.com/gopxl/beep" + +func main() { + // here we go! +} +``` + +Put some MP3/WAV/OGG/FLAC song in the directory of your program. I put `Rockafeller Skank.mp3` by the awesome [Fatboy Slim](https://en.wikipedia.org/wiki/Fatboy_Slim). You can put any song you like. + +Now we need to open the file, so that we can decode and play it. We do this simply with the standard [`os.Open`](https://golang.org/pkg/os/#Open): + +```go +package main + +import ( + "log" + "os" + + "github.com/gopxl/beep" +) + +func main() { + f, err := os.Open("Rockafeller Skank.mp3") + if err != nil { + log.Fatal(err) + } +} + +``` + +Since my file is an MP3, I import [`github.com/gopxl/beep/mp3`](https://godoc.org/github.com/gopxl/beep/mp3) and decode it with `mp3.Decode`. If your file is a WAV, use [`github.com/gopxl/beep/wav`](https://godoc.org/github.com/gopxl/beep/wav) and similarly with other formats. + +```go + 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) + } +``` + +The function `mp3.Decode` returned two very interesting values and one uninteresting error. The first one - the `streamer` - is something we can use to actually play the song. The second one - the `format` - tells us something about the song, most importantly, its [sample rate](https://en.wikipedia.org/wiki/Sample_rate). + +> **Before we get into the rough action, here's an important fact:** `mp3.Decode` _does not_ immediately read and decode the file. It simply returns a streamer that does the reading and decoding on-line (when needed). That way, you can actually stream gigabytes long files directly from the disk consuming almost no RAM. The main consequence/gotcha is that you can't close the file `f` before you finish streaming it. In fact, don't close it at all. Use `streamer.Close()` instead, it'll take care of that. (You can load a file into the memory with [Buffer](https://godoc.org/github.com/gopxl/beep#Buffer) as we'll learn later.) + +And we'll do exactly that: + +```go + streamer, format, err := mp3.Decode(f) + if err != nil { + log.Fatal(err) + } + defer streamer.Close() +``` + +Now, **what's a streamer**? Well, for one, [it's an interface](https://godoc.org/github.com/gopxl/beep#Streamer). You can think of it as an [`io.Reader`](https://golang.org/pkg/io/#Reader) for audio samples. + +> **What are audio samples?** I'm sure you're familiar with the concept of a sound wave. It indicates the air pressure at any point of time. Samples are used to store this sound wave by storing the air pressure at discrete, evenly spaced points of time. If we store the air pressure 44100 times per second, then the sample rate is 44100 samples per second. +> +> In Beep, samples are represented by the type `[2]float64`. They are two floats, because one float is for the left speaker and the other one is for the right speaker. `Streamers`'s analogue of the `io.Reader`'s `Read` method is the `Stream` method, which has this signature: `Stream(samples [][2]float64) (n int, ok bool)`. It looks very much like `Read`, takes a slice of samples, fills it, and returns how many samples it filled. + +One important thing about a streamer is that it drains, just like an `io.Reader`, once you stream it, it's gone. Of course, `mp3.Decode` returns a [`beep.StreamSeeker`](https://godoc.org/github.com/gopxl/beep#StreamSeeker), so we can rewind it back to the beginning and play it again. The main point is that a `Streamer` is stateful, like an audio tape. + +Okay, onto waking up the neighbors! + +Beep has a dedicated [`github.com/gopxl/beep/speaker`](https://godoc.org/github.com/gopxl/beep/speaker) package for blasting sound, which uses [Oto](https://github.com/hajimehoshi/oto) under the hood. The first thing we need to do is to initialize the speaker. + +```go + 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)) +``` + +Whoa, there's a lot going on here! The function `speaker.Init` takes two arguments: the sample rate, and the buffer size. + +**The sample rate argument** simply tells the speaker how quickly it should push the samples to the output. We tell it to do it at exactly `format.SampleRate` samples per second, so that the song plays at the correct speed. + +**The second argument** is the buffer size. This is the number of samples the speaker stores before putting them out. This is to avoid glitches in the playback when the program isn't perfectly synchronized with the speaker. _Larger the buffer, better the stability. Smaller the buffer, lower the latency._ We calculate the size of the buffer using the [`SampleRate.N`](https://godoc.org/github.com/gopxl/beep#SampleRate.N) (`N` stands for _number_), which calculates the number of samples contained in the provided duration. There's an inverse [`SampleRate.D`](https://godoc.org/github.com/gopxl/beep#SampleRate.D) method. We chose the buffer size of 1/10 of a second. + +> **An important notice:** Don't call `speaker.Init` each time you want to play something! Generally, you only want to call it once at the beginning of your program. Calling it again and again will reset the speaker, preventing you from playing multiple sounds simultaneously. + +Now, we can finally play the streamer! + +```go + speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) + + speaker.Play(streamer) +``` + +**Let's run it!** + +``` +$ go run hello-beep.go +$ +``` + +Nothing? The program just finished immediately. + +That's because [`speaker.Play`](https://godoc.org/github.com/gopxl/beep/speaker#Play) is an asynchronous call. It _starts_ playing the streamer, but doesn't wait until it finishes playing. We can fix this temporarily with `select {}` which makes the program hang forever: + +```go + speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) + + speaker.Play(streamer) + select {} +``` + +**Run it again!** + +``` +$ go run hello-beep.go +``` + +Now it works! Perfect. + +> **Note:** You can easily play multiple streamers simultaneously, simply by sending all of them to the speaker with `speaker.Play`. + +But it's kinda ugly. We can fix it! Beep provides and function called [`beep.Seq`](https://godoc.org/github.com/gopxl/beep#Seq) that takes multiple streamers and returns a new streamer that plays them one by one. How is that useful for us now? Beep also provides another function called [`beep.Callback`](https://godoc.org/github.com/gopxl/beep#Callback) which takes a function and returns a streamer that doesn't really play anything, but instead calls the given function. Combining `beep.Seq` and `beep.Callback`, we can make our program wait until the song finishes: + +```go + 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 +``` + +What have we done? We've told the speaker to play a sequence of two streamers: one is our song. Now, when the song finishes, the callback streamer starts playing. It doesn't play anything but instead sends a value over the `done` channel, causing our program to finish. Neat! + +> **Why not make the `speaker.Play` blocking instead?** The reason for this design choice is that making `speaker.Play` blocking would possibly result in goroutine leaks when dealing with various composite streamers. We'll learn about those in the next part. + +When we run the program now, it hangs until the song finishes playing, then quits. Exactly as we intended! + +## Dealing with different sample rates + +In the code above, we explicitly initialized the speaker to use the same sample rate as the song we loaded. But what if that's not the case? What if the speaker has a different sample rate than the audio file? This can happen particularly when we have multiple audio files, each with a different sample rate. + +**Let's see what happens!** + +Change the speaker initialization to this: + +```go + sr := format.SampleRate * 2 + speaker.Init(sr, sr.N(time.Second/10)) +``` + +We've doubled the sample rate. + +``` +$ go run hello-beep.go +``` + +Unsurprisingly, it plays at _double speed_! What can we do about it? Well, for one we can enjoy the fun! But we can also fix it with [`beep.Resample`](https://godoc.org/github.com/gopxl/beep#Resample): + +```go + resampled := beep.Resample(4, format.SampleRate, sr, streamer) + + done := make(chan bool) + speaker.Play(beep.Seq(resampled, beep.Callback(func() { + done <- true + }))) + + <-done +``` + +Now we're playing the `resampled` streamer instead of the original one. The `beep.Resample` function takes four arguments: quality index, the old sample rate, the new sample rate, and a streamer. + +It returns a new steamer, which plays the provided streamer in the new sample rate, assuming the original streamer was in the old sample rate. You can learn more about the quality index in the [documentation of the function](https://godoc.org/github.com/gopxl/beep#Resample), but simply put, if you put a larger quality index, you'll get better quality but more CPU consumption and vice versa. The value of `4` is a reasonable value for real-time, good-quality resampling. + +Now it plays in the original speed as before! So, that's how you deal with sample rates. + +You can also use `beep.Resample`, and especially it's variant [`beep.ResampleRatio`](https://godoc.org/github.com/gopxl/beep#ResampleRatio), to speed up and slow down audio. Try it with your favorite songs, it's really cool! The resampler has a very good quality too. + +Alright, that's all for this part, see you in the next one! \ No newline at end of file diff --git a/docs/Home.md b/docs/Home.md new file mode 100644 index 0000000..a0e2193 --- /dev/null +++ b/docs/Home.md @@ -0,0 +1,11 @@ +# Beep Tutorial + +Hi! + +**Beep** is a simple, fast, and very flexible little library for playing and manipulating sounds in any Go application. + +Although Beep is easy to use, a tutorial is always helpful. + +Choose the first part of the tutorial from the panel on the right. + +For more details about the specifics of Beep API, visit [GoDoc](https://godoc.org/github.com/gopxl/beep). \ No newline at end of file diff --git a/docs/Making-own-streamers.md b/docs/Making-own-streamers.md new file mode 100644 index 0000000..27195d1 --- /dev/null +++ b/docs/Making-own-streamers.md @@ -0,0 +1,211 @@ +Beep offers a lot of pre-made streamers, but sometimes that's not enough. Fortunately, making new ones isn't very hard and in this part, we'll learn just that. + +So, what's a streamer? It's this interface: + +```go +type Streamer interface { + Stream(samples [][2]float64) (n int, ok bool) + Err() error +} +``` + +Read [the docs](https://godoc.org/github.com/gopxl/beep#Streamer) 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`](https://godoc.org/github.com/gopxl/beep#Mixer), which mixes multiple streamers together and makes it possible to add streamers dynamically to it. The [`speaker`](https://godoc.org/github.com/gopxl/beep/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 the error 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 any interface, we need to make a type. The noise generator requires no state, so it'll be an empty struct: + +```go +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: + +```go +func (no 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`: + +```go +func (no Noise) Err() error { + return nil +} +``` + +Now it's done and we can use it in a program: + +```go +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`](https://godoc.org/github.com/gopxl/beep#Take): + +```go +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`](https://godoc.org/github.com/gopxl/beep#StreamerFunc), which is defined like this: + +```go +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: + +```go +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? + +Remember [`beep.Seq`](https://godoc.org/github.com/gopxl/beep#Seq)? It takes a bunch of streamers and streams them one by one. Here, we'll do something similar, but dynamic. We'll make a queue. + +Initially, it'll be empty and it'll stream silence. But, we'll be able to use its `Add` method to add streamers to it. It'll add them to the queue and play them one by one. We will be able to call `Add` at any time and add more songs to the queue. They'll start playing when all the previous songs finish. + +Let's get to it! The `Queue` type needs just one thing to remember: the streamers left to play. + +```go +type Queue struct { + streamers []beep.Streamer +} +``` + +We need a method to add new streamers to the queue: + +```go +func (q *Queue) Add(streamers ...beep.Streamer) { + q.streamers = append(q.streamers, streamers...) +} +``` + +Now, all that's remaining is to implement the streamer interface. Here's the `Stream` method with comments for understanding: + +```go +func (q *Queue) Stream(samples [][2]float64) (n int, ok bool) { + // We use the filled variable to track how many samples we've + // successfully filled already. We loop until all samples are filled. + filled := 0 + for filled < len(samples) { + // There are no streamers in the queue, so we stream silence. + if len(q.streamers) == 0 { + for i := range samples[filled:] { + samples[i][0] = 0 + samples[i][1] = 0 + } + break + } + + // We stream from the first streamer in the queue. + n, ok := q.streamers[0].Stream(samples[filled:]) + // If it's drained, we pop it from the queue, thus continuing with + // the next streamer. + if !ok { + q.streamers = q.streamers[1:] + } + // We update the number of filled samples. + filled += n + } + return len(samples), true +} +``` + +And here's the trivial `Err` method: + +```go +func (q *Queue) Err() error { + return nil +} +``` + +Alright! Now we've gotta use the queue somehow. Here's how we're gonna use it: we'll let the user type the name of a file on the command line and we'll load the file and add it to the queue. If there were no songs in the queue before, it'll start playing immediately. Of course, it'll be a little cumbersome, because there will be no tab-completion, but whatever, it'll be something. + +Here's how it's done (again, with comments): + +```go +func main() { + sr := beep.SampleRate(44100) + speaker.Init(sr, sr.N(time.Second/10)) + + // A zero Queue is an empty Queue. + var queue Queue + speaker.Play(&queue) + + for { + var name string + fmt.Print("Type an MP3 file name: ") + fmt.Scanln(&name) + + // Open the file on the disk. + f, err := os.Open(name) + if err != nil { + fmt.Println(err) + continue + } + + // Decode it. + streamer, format, err := mp3.Decode(f) + if err != nil { + fmt.Println(err) + continue + } + + // The speaker's sample rate is fixed at 44100. Therefore, we need to + // resample the file in case it's in a different sample rate. + resampled := beep.Resample(4, format.SampleRate, sr, streamer) + + // And finally, we add the song to the queue. + speaker.Lock() + queue.Add(resampled) + speaker.Unlock() + } +} +``` + +And that's it! + +We've learned a lot today. _Now, go, brave programmer, make new streamers, make new music players, make new artificial sound generators, whatever, go make the world a better place!_ + +> **Why isn't the `Queue` type implemented in Beep?** So that I could make this tutorial. \ No newline at end of file diff --git a/docs/To-buffer,-or-not-to-buffer,-that-is-the-question.md b/docs/To-buffer,-or-not-to-buffer,-that-is-the-question.md new file mode 100644 index 0000000..cdc5dfd --- /dev/null +++ b/docs/To-buffer,-or-not-to-buffer,-that-is-the-question.md @@ -0,0 +1,102 @@ +The root data source of all audio we've worked with so far was an audio file. The audio file had to be open the whole time its content was being played over the speaker. Obviously, this isn't always desirable. A good example is a gunshot sound effect in an action game. It's a small file, there's no reason to stream it directly from the disk. It's much better to have it loaded in memory. Furthermore, there may be gunshots all over the place. Decoding a file gives us only one streamer, which limits us to only one gunshot sound playing at any moment. We could open the file multiple times, but you can surely see that's a wrong way to do it. + +In this part, we'll learn how to load a sound to memory and then stream it from there. + +**We'll fire guns today!** Go ahead and download some gunshot sound (for example [from here](https://www.freesoundeffects.com/)). We'll start by loading and decoding the sound: + +```go +package main + +import ( + "log" + "os" + + "github.com/gopxl/beep/mp3" +) + +func main() { + f, err := os.Open("gunshot.mp3") + if err != nil { + log.Fatal(err) + } + + streamer, format, err := mp3.Decode(f) + if err != nil { + log.Fatal(err) + } + + speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) + + // TODO +} +``` + +Notice that we omitted the `defer streamer.Close()` line. That's because we actually will be closing the streamer before finishing the program. + +Now, to load audio into memory, we obviously need to store it somewhere. Beep has us covered with [`beep.Buffer`](https://godoc.org/github.com/gopxl/beep#Buffer). Don't confuse it with the speaker's buffer, whose size we set with `speaker.Init`. This buffer is very much like [`bytes.Buffer`](https://golang.org/pkg/bytes/#Buffer), except is a for storing samples, not bytes, and is simpler. + +First, we need to create a buffer (we don't really need to initialize the speaker before making the buffer): + +```go + streamer, format, err := mp3.Decode(f) + if err != nil { + log.Fatal(err) + } + + speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) + + buffer := beep.NewBuffer(format) +``` + +A buffer doesn't store samples as a slice of `[2]float64`s, because that would take up too much space. Instead, it encodes them as bytes. That's why it requires a format. Aside from sample rate (which is not used by buffer), [`beep.Format`](https://godoc.org/github.com/gopxl/beep#Format) specifies the number of channels and the number of bytes per sample. Those are used to determine how to encode the samples. + +We'll use the same format as the loaded audio file. That way we won't lose any quality. + +Now we need to put the contents of `streamer` inside `buffer`. How do we do that? We do that with [`buffer.Append`](https://godoc.org/github.com/gopxl/beep#Buffer.Append). + +```go + buffer := beep.NewBuffer(format) + buffer.Append(streamer) +``` + +Calling `Append` will _stream_ (not play out loud) the whole streamer and append all its content to the buffer. You could append multiple streamers to the same buffer if you wanted to. The call to `Append` is blocking - it doesn't return before all of the streamer's content is streamed. Of course, this streaming is done as quickly as possible, it'll take no time. + +At this point, `streamer` is drained and no longer needed. We can close it (that closes the source file as well): + +```go + buffer := beep.NewBuffer(format) + buffer.Append(streamer) + streamer.Close() +``` + +Good. Now that we've loaded the audio into memory, how do we play it? It's easy. Buffer has a special method called [`Streamer`](https://godoc.org/github.com/gopxl/beep#Buffer.Streamer). It takes two `int`s specifying the interval of the buffer's samples we'd like to stream and returns a [`beep.StreamSeeker`](https://godoc.org/github.com/gopxl/beep#StreamSeeker) that streams that interval. + +> **Note:** Because it returns a `beep.StreamSeeker`, we can loop it and rewind it as we like. + +Creating these streamers is very cheap and we can make as many of them as we like. That way, we can play many gunshots at the same time. + +So, let's do that! Let's make it so that entering a newline will fire a gunshot! That should be easy to do: + +```go + buffer := beep.NewBuffer(format) + buffer.Append(streamer) + streamer.Close() + + for { + fmt.Print("Press [ENTER] to fire a gunshot! ") + fmt.Scanln() + + shot := buffer.Streamer(0, buffer.Len()) + speaker.Play(shot) + } +``` + +First, we've created a streamer with `buffer.Streamer`. We set the interval to all contents of the buffer. Then we sent the streamer to the speaker and that's it! + +> **If you feel like the latency is too big:** Try lowering the speaker buffer size from `time.Second/10` to `time.Second/30` or even `time.Second/100`. + +## When to buffer and when not to? + +Storing audio in memory is usually useful with sounds you want to play many times or multiple instances at the same time. Also, it's fine if the file isn't too large. + +On the other hand, streaming from the disk is good when you only want to play one instance at any moment or when the file is very big. Streaming directly from the disk minimizes memory usage and startup time. \ No newline at end of file diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md new file mode 100644 index 0000000..7712de5 --- /dev/null +++ b/docs/_Sidebar.md @@ -0,0 +1,6 @@ +### [Home](https://github.com/gopxl/beep/wiki) + +### [Hello, Beep!](https://github.com/gopxl/beep/wiki/Hello,-Beep!) +### [Composing and controlling](https://github.com/gopxl/beep/wiki/Composing-and-controlling) +### [To buffer, or not to buffer, that is the question](https://github.com/gopxl/beep/wiki/To-buffer,-or-not-to-buffer,-that-is-the-question) +### [Making own streamers](https://github.com/gopxl/beep/wiki/Making-own-streamers) \ No newline at end of file From 674c61dce57badb85bf671a308262c732796c590 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Sun, 20 Oct 2024 21:58:03 +0200 Subject: [PATCH 2/3] Refactor docs in preparation for docgen --- docs/{Home.md => Tutorial/01. Home.md} | 0 docs/{Hello,-Beep!.md => Tutorial/02. Hello, Beep!.md} | 2 ++ .../03. Composing and Controlling.md} | 2 ++ .../04. Making Own Streamers.md} | 2 ++ .../04. To Buffer, or Not To Buffer.md} | 2 ++ docs/_Sidebar.md | 6 ------ 6 files changed, 8 insertions(+), 6 deletions(-) rename docs/{Home.md => Tutorial/01. Home.md} (100%) rename docs/{Hello,-Beep!.md => Tutorial/02. Hello, Beep!.md} (99%) rename docs/{Composing-and-controlling.md => Tutorial/03. Composing and Controlling.md} (99%) rename docs/{Making-own-streamers.md => Tutorial/04. Making Own Streamers.md} (99%) rename docs/{To-buffer,-or-not-to-buffer,-that-is-the-question.md => Tutorial/04. To Buffer, or Not To Buffer.md} (98%) delete mode 100644 docs/_Sidebar.md diff --git a/docs/Home.md b/docs/Tutorial/01. Home.md similarity index 100% rename from docs/Home.md rename to docs/Tutorial/01. Home.md diff --git a/docs/Hello,-Beep!.md b/docs/Tutorial/02. Hello, Beep!.md similarity index 99% rename from docs/Hello,-Beep!.md rename to docs/Tutorial/02. Hello, Beep!.md index 6f4e8f1..2cd1a5f 100644 --- a/docs/Hello,-Beep!.md +++ b/docs/Tutorial/02. Hello, Beep!.md @@ -1,3 +1,5 @@ +# Hello, Beep! + Welcome to the Beep tutorial! In this part, we'll learn how to load a song, initialize the speaker, and wake up your neighbors. The first thing we obviously need is the [Beep library](https://github.com/gopxl/beep/) (I expect you have the Go programming language installed), which you can install with this command: diff --git a/docs/Composing-and-controlling.md b/docs/Tutorial/03. Composing and Controlling.md similarity index 99% rename from docs/Composing-and-controlling.md rename to docs/Tutorial/03. Composing and Controlling.md index 703227d..4c941b7 100644 --- a/docs/Composing-and-controlling.md +++ b/docs/Tutorial/03. Composing and Controlling.md @@ -1,3 +1,5 @@ +# Composing and Controlling + 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: diff --git a/docs/Making-own-streamers.md b/docs/Tutorial/04. Making Own Streamers.md similarity index 99% rename from docs/Making-own-streamers.md rename to docs/Tutorial/04. Making Own Streamers.md index 27195d1..b51f69e 100644 --- a/docs/Making-own-streamers.md +++ b/docs/Tutorial/04. Making Own Streamers.md @@ -1,3 +1,5 @@ +# Making own streamers + Beep offers a lot of pre-made streamers, but sometimes that's not enough. Fortunately, making new ones isn't very hard and in this part, we'll learn just that. So, what's a streamer? It's this interface: diff --git a/docs/To-buffer,-or-not-to-buffer,-that-is-the-question.md b/docs/Tutorial/04. To Buffer, or Not To Buffer.md similarity index 98% rename from docs/To-buffer,-or-not-to-buffer,-that-is-the-question.md rename to docs/Tutorial/04. To Buffer, or Not To Buffer.md index cdc5dfd..4a513ec 100644 --- a/docs/To-buffer,-or-not-to-buffer,-that-is-the-question.md +++ b/docs/Tutorial/04. To Buffer, or Not To Buffer.md @@ -1,3 +1,5 @@ +# To buffer, or not to buffer, that is the question + The root data source of all audio we've worked with so far was an audio file. The audio file had to be open the whole time its content was being played over the speaker. Obviously, this isn't always desirable. A good example is a gunshot sound effect in an action game. It's a small file, there's no reason to stream it directly from the disk. It's much better to have it loaded in memory. Furthermore, there may be gunshots all over the place. Decoding a file gives us only one streamer, which limits us to only one gunshot sound playing at any moment. We could open the file multiple times, but you can surely see that's a wrong way to do it. In this part, we'll learn how to load a sound to memory and then stream it from there. diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md deleted file mode 100644 index 7712de5..0000000 --- a/docs/_Sidebar.md +++ /dev/null @@ -1,6 +0,0 @@ -### [Home](https://github.com/gopxl/beep/wiki) - -### [Hello, Beep!](https://github.com/gopxl/beep/wiki/Hello,-Beep!) -### [Composing and controlling](https://github.com/gopxl/beep/wiki/Composing-and-controlling) -### [To buffer, or not to buffer, that is the question](https://github.com/gopxl/beep/wiki/To-buffer,-or-not-to-buffer,-that-is-the-question) -### [Making own streamers](https://github.com/gopxl/beep/wiki/Making-own-streamers) \ No newline at end of file From d7dfbea00ee13ec764cea7bf1fda0d78971b0387 Mon Sep 17 00:00:00 2001 From: Mark Kremer Date: Sun, 20 Oct 2024 22:01:08 +0200 Subject: [PATCH 3/3] Add Github Workflow for docgen --- .github/workflows/github-pages.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/github-pages.yml diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml new file mode 100644 index 0000000..6c9d0df --- /dev/null +++ b/.github/workflows/github-pages.yml @@ -0,0 +1,18 @@ +name: GitHub Pages + +on: + push: + branches: ['main'] + tags: ['*'] + +jobs: + upload-docs: + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + steps: + - uses: gopxl/docs@main + with: + docs-directory: 'docs/' + main-branch: 'main'