├── .github ├── dependabot.yml └── workflows │ ├── github-pages.yml │ └── go.yml ├── LICENSE ├── README.md ├── buffer.go ├── buffer_test.go ├── compositors.go ├── compositors_test.go ├── ctrl.go ├── ctrl_test.go ├── docs └── Tutorial │ ├── 01. Home.md │ ├── 02. Hello, Beep!.md │ ├── 03. Composing and Controlling.md │ ├── 04. Making Own Streamers.md │ └── 04. To Buffer, or Not To Buffer.md ├── effects ├── doc.go ├── doppler.go ├── equalizer.go ├── gain.go ├── mono.go ├── pan.go ├── swap.go ├── transition.go ├── transition_test.go └── volume.go ├── examples ├── doppler-stereo-room │ ├── README.md │ ├── main.go │ └── screenshot.png ├── midi │ ├── Buy to the Beat - V2 License.md │ ├── Buy to the Beat - V2.mid │ ├── Florestan-Basic-GM-GS License.md │ ├── Florestan-Basic-GM-GS-by-Nando-Florestan(Public-Domain).sf2 │ └── main.go ├── speedy-player │ ├── README.md │ ├── main.go │ └── screenshot.png ├── tone-player │ └── main.go └── tutorial │ ├── 1-hello-beep │ ├── Lame_Drivers_-_01_-_Frozen_Egg.mp3 │ ├── a │ │ └── main.go │ └── b │ │ └── main.go │ ├── 2-composing-and-controlling │ ├── Miami_Slice_-_04_-_Step_Into_Me.mp3 │ ├── a │ │ └── main.go │ ├── b │ │ └── main.go │ ├── c │ │ └── main.go │ └── d │ │ └── main.go │ ├── 3-to-buffer-or-not-to-buffer │ ├── gunshot.mp3 │ └── main.go │ ├── 4-making-own-streamers │ ├── a │ │ └── main.go │ └── b │ │ ├── frozen-egg.mp3 │ │ ├── main.go │ │ └── step-into-me.mp3 │ ├── 5-equalizer │ ├── mono │ │ └── main.go │ └── stereo │ │ └── main.go │ └── README.md ├── flac ├── decode.go ├── decode_test.go └── doc.go ├── generators ├── sawtooth.go ├── silence.go ├── silence_test.go ├── sine.go ├── sine_test.go ├── square.go └── triangle.go ├── go.mod ├── go.sum ├── interface.go ├── internal ├── testdata │ ├── valid_44100hz_22050_samples.ogg │ ├── valid_44100hz_22050_samples.wav │ ├── valid_44100hz_22050_samples_ffmpeg.flac │ └── valid_44100hz_x_padded_samples.mp3 ├── testtools │ ├── asserts.go │ ├── sinks.go │ ├── streamers.go │ └── testdata.go └── util │ ├── math.go │ └── math_test.go ├── midi └── decode.go ├── mixer.go ├── mixer_test.go ├── mp3 ├── decode.go └── decode_test.go ├── resample.go ├── resample_test.go ├── speaker ├── speaker.go └── speaker_test.go ├── streamers.go ├── vorbis ├── decode.go └── decode_test.go └── wav ├── decode.go ├── decode_test.go ├── doc.go ├── encode.go └── encode_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | workflow_dispatch: {} 5 | push: 6 | branches: ['main'] 7 | tags: ['*'] 8 | 9 | jobs: 10 | upload-docs: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | pages: write 14 | id-token: write 15 | env: 16 | DOCS_DIR: 'docs/' 17 | OUTPUT_DIR: '_site/' 18 | MAIN_BRANCH: 'main' 19 | steps: 20 | - name: Configure pages 21 | uses: actions/configure-pages@v5 22 | id: configure-pages 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | with: 26 | ref: ${{ env.MAIN_BRANCH }} 27 | fetch-depth: 0 # checkout a non-shallow copy so the generator can generate docs for all major versions 28 | - uses: gopxl/docs@main 29 | with: 30 | site-url: ${{ steps.configure-pages.outputs.base_url }} 31 | docs-directory: ${{ env.DOCS_DIR }} 32 | output-directory: ${{ env.OUTPUT_DIR }} 33 | main-branch: ${{ env.MAIN_BRANCH }} 34 | - name: Upload pages 35 | uses: actions/upload-pages-artifact@v3 36 | with: 37 | path: ${{ env.OUTPUT_DIR }} 38 | - name: Deploy pages 39 | uses: actions/deploy-pages@v4 40 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | go: 19 | - '1.21' # minimum supported version 20 | - 'stable' 21 | name: "build with Go ${{ matrix.go }}" 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version: ${{ matrix.go }} 29 | 30 | - name: Install goveralls 31 | if: ${{ github.ref == 'refs/heads/main' }} 32 | run: go install github.com/mattn/goveralls@latest 33 | 34 | # Install ALSA for building Oto 35 | - name: Install ALSA 36 | run: sudo apt install libasound2-dev 37 | 38 | - name: Build 39 | run: go build -v ./... 40 | 41 | - name: Test 42 | run: go test -v -covermode atomic -coverprofile=covprofile ./... 43 | 44 | - name: Gofmt 45 | # Run gofmt, print the output and exit with status code 1 if it isn't empty. 46 | run: | 47 | OUTPUT=$(gofmt -d ./) 48 | echo "$OUTPUT" 49 | test -z "$OUTPUT" 50 | 51 | - name: Send coverage 52 | if: ${{ github.ref == 'refs/heads/main' }} 53 | env: 54 | COVERALLS_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 55 | run: goveralls -coverprofile=covprofile -service=github 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Michal Štrba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Beep 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/gopxl/beep/v2.svg)](https://pkg.go.dev/github.com/gopxl/beep/v2) 4 | [![Go build status](https://github.com/gopxl/beep/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/gopxl/beep/actions/workflows/go.yml?query=branch%3Amain) 5 | [![Coverage Status](https://coveralls.io/repos/github/gopxl/beep/badge.svg?branch=main)](https://coveralls.io/github/gopxl/beep?branch=main) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/gopxl/beep/v2)](https://goreportcard.com/report/github.com/gopxl/beep/v2) 7 | [![Discord Chat](https://img.shields.io/discord/1158461233121468496)](https://discord.gg/hPBTTXGDU3) 8 | 9 | 10 | A little package that brings sound to any Go application. Suitable for playback and audio-processing. 11 | 12 | ``` 13 | go get -u github.com/gopxl/beep/v2 14 | ``` 15 | 16 | ## Features 17 | 18 | Beep is built on top of its [Streamer](https://godoc.org/github.com/gopxl/beep#Streamer) interface, which is like [io.Reader](https://golang.org/pkg/io/#Reader), but for audio. It was one of the best design decisions I've ever made and it enabled all the rest of the features to naturally come together with not much code. 19 | 20 | - **Decode and play WAV, MP3, OGG, FLAC and MIDI.** 21 | - **Encode and save WAV.** 22 | - **Very simple API.** Limiting the support to stereo (two channel) audio made it possible to simplify the architecture and the API. 23 | - **Rich library of compositors and effects.** Loop, pause/resume, change volume, mix, sequence, change playback speed, and more. 24 | - **Easily create new effects.** With the `Streamer` interface, creating new effects is very easy. 25 | - **Generate completely own artificial sounds.** Again, the `Streamer` interface enables easy sound generation. 26 | - **Very small codebase.** The core is just ~1K LOC. 27 | 28 | ## Tutorial 29 | 30 | The [Wiki](https://github.com/gopxl/beep/wiki) contains a handful of tutorials for you to get started. They teach the fundamentals and advanced topics alike. **Read them especially if you call `speaker.Init` every time you play something.** 31 | 32 | - [Hello, Beep!](https://github.com/gopxl/beep/wiki/Hello,-Beep!) 33 | - [Composing and controlling](https://github.com/gopxl/beep/wiki/Composing-and-controlling) 34 | - [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) 35 | - [Making own streamers](https://github.com/gopxl/beep/wiki/Making-own-streamers) 36 | 37 | ## Examples 38 | 39 | | [Speedy Player](https://github.com/gopxl/beep/tree/main/examples/speedy-player) | [Doppler Stereo Room](https://github.com/gopxl/beep/tree/main/examples/doppler-stereo-room) | 40 | |-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| 41 | | ![Speedy Player](https://github.com/gopxl/beep/blob/main/examples/speedy-player/screenshot.png) | ![Doppler Stereo Room](https://github.com/gopxl/beep/blob/main/examples/doppler-stereo-room/screenshot.png) | 42 | 43 | ## Dependencies 44 | 45 | For playback, Beep uses [Oto](https://github.com/hajimehoshi/oto) under the hood. Check its requirements to see what you need to install for building your application. 46 | 47 | Running an already built application should work with no extra dependencies. 48 | 49 | ## License 50 | 51 | [MIT](https://github.com/gopxl/beep/blob/main/LICENSE) 52 | 53 | ## Related projects 54 | 55 | - [Microphone support for Beep (a wrapper around PortAudio)](https://github.com/MarkKremer/microphone) 56 | 57 | ## Projects using Beep 58 | - [retro](https://github.com/Malwarize/retro) 59 | - [Mifasol music server](https://github.com/jypelle/mifasol) 60 | -------------------------------------------------------------------------------- /buffer.go: -------------------------------------------------------------------------------- 1 | package beep 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | 8 | "github.com/gopxl/beep/v2/internal/util" 9 | ) 10 | 11 | // SampleRate is the number of samples per second. 12 | type SampleRate int 13 | 14 | // D returns the duration of n samples. 15 | func (sr SampleRate) D(n int) time.Duration { 16 | return time.Second * time.Duration(n) / time.Duration(sr) 17 | } 18 | 19 | // N returns the number of samples that last for d duration. 20 | func (sr SampleRate) N(d time.Duration) int { 21 | return int(d * time.Duration(sr) / time.Second) 22 | } 23 | 24 | // Format is the format of a Buffer or another audio source. 25 | type Format struct { 26 | // SampleRate is the number of samples per second. 27 | SampleRate SampleRate 28 | 29 | // NumChannels is the number of channels. The value of 1 is mono, the value of 2 is stereo. 30 | // The samples should always be interleaved. 31 | NumChannels int 32 | 33 | // Precision is the number of bytes used to encode a single sample. Only values up to 6 work 34 | // well, higher values lose precision due to floating point numbers. 35 | Precision int 36 | } 37 | 38 | // Width returns the number of bytes per one frame (samples in all channels). 39 | // 40 | // This is equal to f.NumChannels * f.Precision. 41 | func (f Format) Width() int { 42 | return f.NumChannels * f.Precision 43 | } 44 | 45 | // EncodeSigned encodes a single sample in f.Width() bytes to p in signed format. 46 | func (f Format) EncodeSigned(p []byte, sample [2]float64) (n int) { 47 | return f.encode(true, p, sample) 48 | } 49 | 50 | // EncodeUnsigned encodes a single sample in f.Width() bytes to p in unsigned format. 51 | func (f Format) EncodeUnsigned(p []byte, sample [2]float64) (n int) { 52 | return f.encode(false, p, sample) 53 | } 54 | 55 | // DecodeSigned decodes a single sample encoded in f.Width() bytes from p in signed format. 56 | func (f Format) DecodeSigned(p []byte) (sample [2]float64, n int) { 57 | return f.decode(true, p) 58 | } 59 | 60 | // DecodeUnsigned decodes a single sample encoded in f.Width() bytes from p in unsigned format. 61 | func (f Format) DecodeUnsigned(p []byte) (sample [2]float64, n int) { 62 | return f.decode(false, p) 63 | } 64 | 65 | func (f Format) encode(signed bool, p []byte, sample [2]float64) (n int) { 66 | switch { 67 | case f.NumChannels == 1: 68 | x := util.Clamp((sample[0]+sample[1])/2, -1, 1) 69 | p = p[encodeFloat(signed, f.Precision, p, x):] 70 | case f.NumChannels >= 2: 71 | for c := range sample { 72 | x := util.Clamp(sample[c], -1, 1) 73 | p = p[encodeFloat(signed, f.Precision, p, x):] 74 | } 75 | for c := len(sample); c < f.NumChannels; c++ { 76 | p = p[encodeFloat(signed, f.Precision, p, 0):] 77 | } 78 | default: 79 | panic(fmt.Errorf("format: encode: invalid number of channels: %d", f.NumChannels)) 80 | } 81 | return f.Width() 82 | } 83 | 84 | func (f Format) decode(signed bool, p []byte) (sample [2]float64, n int) { 85 | switch { 86 | case f.NumChannels == 1: 87 | x, _ := decodeFloat(signed, f.Precision, p) 88 | return [2]float64{x, x}, f.Width() 89 | case f.NumChannels >= 2: 90 | for c := range sample { 91 | x, n := decodeFloat(signed, f.Precision, p) 92 | sample[c] = x 93 | p = p[n:] 94 | } 95 | for c := len(sample); c < f.NumChannels; c++ { 96 | _, n := decodeFloat(signed, f.Precision, p) 97 | p = p[n:] 98 | } 99 | return sample, f.Width() 100 | default: 101 | panic(fmt.Errorf("format: decode: invalid number of channels: %d", f.NumChannels)) 102 | } 103 | } 104 | 105 | func encodeFloat(signed bool, precision int, p []byte, x float64) (n int) { 106 | var xUint64 uint64 107 | if signed { 108 | xUint64 = floatToSigned(precision, x) 109 | } else { 110 | xUint64 = floatToUnsigned(precision, x) 111 | } 112 | for i := 0; i < precision; i++ { 113 | p[i] = byte(xUint64) 114 | xUint64 >>= 8 115 | } 116 | return precision 117 | } 118 | 119 | func decodeFloat(signed bool, precision int, p []byte) (x float64, n int) { 120 | var xUint64 uint64 121 | for i := precision - 1; i >= 0; i-- { 122 | xUint64 <<= 8 123 | xUint64 += uint64(p[i]) 124 | } 125 | if signed { 126 | return signedToFloat(precision, xUint64), precision 127 | } 128 | return unsignedToFloat(precision, xUint64), precision 129 | } 130 | 131 | func floatToSigned(precision int, x float64) uint64 { 132 | if x < 0 { 133 | compl := uint64(-x * math.Exp2(float64(precision)*8-1)) 134 | return uint64(1<= 1<b.Len() or to= len(bs.data) { 222 | return 0, false 223 | } 224 | for i := range samples { 225 | if bs.pos >= len(bs.data) { 226 | break 227 | } 228 | sample, advance := bs.f.DecodeSigned(bs.data[bs.pos:]) 229 | samples[i] = sample 230 | bs.pos += advance 231 | n++ 232 | } 233 | return n, true 234 | } 235 | 236 | func (bs *bufferStreamer) Err() error { 237 | return nil 238 | } 239 | 240 | func (bs *bufferStreamer) Len() int { 241 | return len(bs.data) / bs.f.Width() 242 | } 243 | 244 | func (bs *bufferStreamer) Position() int { 245 | return bs.pos / bs.f.Width() 246 | } 247 | 248 | func (bs *bufferStreamer) Seek(p int) error { 249 | if p < 0 || bs.Len() < p { 250 | return fmt.Errorf("buffer: seek position %v out of range [%v, %v]", p, 0, bs.Len()) 251 | } 252 | bs.pos = p * bs.f.Width() 253 | return nil 254 | } 255 | -------------------------------------------------------------------------------- /buffer_test.go: -------------------------------------------------------------------------------- 1 | package beep_test 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/gopxl/beep/v2" 11 | "github.com/gopxl/beep/v2/generators" 12 | ) 13 | 14 | type bufferFormatTestCase struct { 15 | Name string 16 | Precision int 17 | NumChannels int 18 | Signed bool 19 | Bytes []byte 20 | Samples [2]float64 21 | SkipDecodeTest bool 22 | } 23 | 24 | var bufferFormatTests = []bufferFormatTestCase{ 25 | // See https://gist.github.com/endolith/e8597a58bcd11a6462f33fa8eb75c43d 26 | // for an explanation about the asymmetry in sample encodings in WAV when 27 | // converting between ints and floats. Note that Beep does not follow the 28 | // suggested solution. Instead, integer samples are divided by 1 more, so 29 | // that the resulting float value falls within the range of -1.0 and 1.0. 30 | // This is similar to how some other tools do the conversion. 31 | { 32 | Name: "1 channel 8bit WAV negative full scale", 33 | Precision: 1, 34 | NumChannels: 1, 35 | Signed: false, 36 | Bytes: []byte{0x00}, 37 | Samples: [2]float64{-1.0, -1.0}, 38 | }, 39 | { 40 | Name: "1 channel 8bit WAV midpoint", 41 | Precision: 1, 42 | NumChannels: 1, 43 | Signed: false, 44 | Bytes: []byte{0x80}, 45 | Samples: [2]float64{0.0, 0.0}, 46 | }, 47 | { 48 | // Because the WAV integer range is asymmetric, converting it to float 49 | // by division will not result in an exactly 1.0 full scale float value. 50 | // It will be 1 least significant bit integer value lower. "1", converted 51 | // to float for an 8-bit WAV sample is 1 / (1 << 7). 52 | Name: "1 channel 8bit WAV positive full scale minus 1 significant bit", 53 | Precision: 1, 54 | NumChannels: 1, 55 | Signed: false, 56 | Bytes: []byte{0xFF}, 57 | Samples: [2]float64{1.0 - (1.0 / (1 << 7)), 1.0 - (1.0 / (1 << 7))}, 58 | }, 59 | { 60 | Name: "2 channel 8bit WAV full scale", 61 | Precision: 1, 62 | NumChannels: 2, 63 | Signed: false, 64 | Bytes: []byte{0x00, 0xFF}, 65 | Samples: [2]float64{-1.0, 1.0 - (1.0 / (1 << 7))}, 66 | }, 67 | { 68 | Name: "1 channel 16bit WAV negative full scale", 69 | Precision: 2, 70 | NumChannels: 1, 71 | Signed: true, 72 | Bytes: []byte{0x00, 0x80}, 73 | Samples: [2]float64{-1.0, -1.0}, 74 | }, 75 | { 76 | Name: "1 channel 16bit WAV midpoint", 77 | Precision: 2, 78 | NumChannels: 1, 79 | Signed: true, 80 | Bytes: []byte{0x00, 0x00}, 81 | Samples: [2]float64{0.0, 0.0}, 82 | }, 83 | { 84 | // Because the WAV integer range is asymmetric, converting it to float 85 | // by division will not result in an exactly 1.0 full scale float value. 86 | // It will be 1 least significant bit integer value lower. "1", converted 87 | // to float for an 16-bit WAV sample is 1 / (1 << 15). 88 | Name: "1 channel 16bit WAV positive full scale minus 1 significant bit", 89 | Precision: 2, 90 | NumChannels: 1, 91 | Signed: true, 92 | Bytes: []byte{0xFF, 0x7F}, 93 | Samples: [2]float64{1.0 - (1.0 / (1 << 15)), 1.0 - (1.0 / (1 << 15))}, 94 | }, 95 | { 96 | Name: "1 channel 8bit WAV float positive full scale clipping test", 97 | Precision: 1, 98 | NumChannels: 1, 99 | Signed: false, 100 | Bytes: []byte{0xFF}, 101 | Samples: [2]float64{1.0, 1.0}, 102 | SkipDecodeTest: true, 103 | }, 104 | { 105 | Name: "1 channel 16bit WAV float positive full scale clipping test", 106 | Precision: 2, 107 | NumChannels: 1, 108 | Signed: true, 109 | Bytes: []byte{0xFF, 0x7F}, 110 | Samples: [2]float64{1.0, 1.0}, 111 | SkipDecodeTest: true, 112 | }, 113 | } 114 | 115 | func TestFormatDecode(t *testing.T) { 116 | for _, test := range bufferFormatTests { 117 | if test.SkipDecodeTest { 118 | continue 119 | } 120 | 121 | t.Run(test.Name, func(t *testing.T) { 122 | format := beep.Format{ 123 | SampleRate: 44100, 124 | Precision: test.Precision, 125 | NumChannels: test.NumChannels, 126 | } 127 | 128 | var sample [2]float64 129 | var n int 130 | if test.Signed { 131 | sample, n = format.DecodeSigned(test.Bytes) 132 | } else { 133 | sample, n = format.DecodeUnsigned(test.Bytes) 134 | } 135 | assert.Equal(t, len(test.Bytes), n) 136 | assert.Equal(t, test.Samples, sample) 137 | }) 138 | } 139 | } 140 | 141 | func TestFormatEncode(t *testing.T) { 142 | for _, test := range bufferFormatTests { 143 | t.Run(test.Name, func(t *testing.T) { 144 | format := beep.Format{ 145 | SampleRate: 44100, 146 | Precision: test.Precision, 147 | NumChannels: test.NumChannels, 148 | } 149 | 150 | bytes := make([]byte, test.Precision*test.NumChannels) 151 | var n int 152 | if test.Signed { 153 | n = format.EncodeSigned(bytes, test.Samples) 154 | } else { 155 | n = format.EncodeUnsigned(bytes, test.Samples) 156 | } 157 | assert.Equal(t, len(test.Bytes), n) 158 | assert.Equal(t, test.Bytes, bytes) 159 | }) 160 | } 161 | } 162 | 163 | func TestFormatEncodeDecode(t *testing.T) { 164 | formats := make(chan beep.Format) 165 | go func() { 166 | defer close(formats) 167 | for _, sampleRate := range []beep.SampleRate{100, 2347, 44100, 48000} { 168 | for _, numChannels := range []int{1, 2, 3, 4} { 169 | for _, precision := range []int{1, 2, 3, 4, 5, 6} { 170 | formats <- beep.Format{ 171 | SampleRate: sampleRate, 172 | NumChannels: numChannels, 173 | Precision: precision, 174 | } 175 | } 176 | } 177 | } 178 | }() 179 | 180 | for format := range formats { 181 | for i := 0; i < 20; i++ { 182 | deviation := 2.0 / (math.Pow(2, float64(format.Precision)*8) - 2) 183 | sample := [2]float64{rand.Float64()*2 - 1, rand.Float64()*2 - 1} 184 | 185 | tmp := make([]byte, format.Width()) 186 | format.EncodeSigned(tmp, sample) 187 | decoded, _ := format.DecodeSigned(tmp) 188 | 189 | if format.NumChannels == 1 { 190 | if math.Abs((sample[0]+sample[1])/2-decoded[0]) > deviation || decoded[0] != decoded[1] { 191 | t.Fatalf("signed decoded sample is too different: %v -> %v (deviation: %v)", sample, decoded, deviation) 192 | } 193 | } else { 194 | if math.Abs(sample[0]-decoded[0]) > deviation || math.Abs(sample[1]-decoded[1]) > deviation { 195 | t.Fatalf("signed decoded sample is too different: %v -> %v (deviation: %v)", sample, decoded, deviation) 196 | } 197 | } 198 | 199 | format.EncodeUnsigned(tmp, sample) 200 | decoded, _ = format.DecodeUnsigned(tmp) 201 | 202 | if format.NumChannels == 1 { 203 | if math.Abs((sample[0]+sample[1])/2-decoded[0]) > deviation || decoded[0] != decoded[1] { 204 | t.Fatalf("unsigned decoded sample is too different: %v -> %v (deviation: %v)", sample, decoded, deviation) 205 | } 206 | } else { 207 | if math.Abs(sample[0]-decoded[0]) > deviation || math.Abs(sample[1]-decoded[1]) > deviation { 208 | t.Fatalf("unsigned decoded sample is too different: %v -> %v (deviation: %v)", sample, decoded, deviation) 209 | } 210 | } 211 | } 212 | } 213 | } 214 | 215 | func TestBufferAppendPop(t *testing.T) { 216 | formats := make(chan beep.Format) 217 | go func() { 218 | defer close(formats) 219 | for _, numChannels := range []int{1, 2, 3, 4} { 220 | formats <- beep.Format{ 221 | SampleRate: 44100, 222 | NumChannels: numChannels, 223 | Precision: 2, 224 | } 225 | } 226 | }() 227 | 228 | for format := range formats { 229 | b := beep.NewBuffer(format) 230 | b.Append(generators.Silence(768)) 231 | if b.Len() != 768 { 232 | t.Fatalf("buffer length isn't equal to appended stream length: expected: %v, actual: %v (NumChannels: %v)", 768, b.Len(), format.NumChannels) 233 | } 234 | b.Pop(512) 235 | if b.Len() != 768-512 { 236 | t.Fatalf("buffer length isn't as expected after Pop: expected: %v, actual: %v (NumChannels: %v)", 768-512, b.Len(), format.NumChannels) 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /compositors.go: -------------------------------------------------------------------------------- 1 | package beep 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // Take returns a Streamer which streams at most num samples from s. 11 | // 12 | // The returned Streamer propagates s's errors through Err. 13 | func Take(num int, s Streamer) Streamer { 14 | return &take{ 15 | s: s, 16 | remains: num, 17 | } 18 | } 19 | 20 | type take struct { 21 | s Streamer 22 | remains int 23 | } 24 | 25 | func (t *take) Stream(samples [][2]float64) (n int, ok bool) { 26 | if t.remains <= 0 { 27 | return 0, false 28 | } 29 | toStream := min(t.remains, len(samples)) 30 | n, ok = t.s.Stream(samples[:toStream]) 31 | t.remains -= n 32 | return n, ok 33 | } 34 | 35 | func (t *take) Err() error { 36 | return t.s.Err() 37 | } 38 | 39 | // Loop takes a StreamSeeker and plays it count times. If count is negative, s is looped infinitely. 40 | // 41 | // The returned Streamer propagates s's errors. 42 | // 43 | // Deprecated: use Loop2 instead. A call to Loop can be rewritten as follows: 44 | // - beep.Loop(-1, s) -> beep.Loop2(s) 45 | // - beep.Loop(0, s) -> no longer supported, use beep.Ctrl instead. 46 | // - beep.Loop(3, s) -> beep.Loop2(s, beep.LoopTimes(2)) 47 | // Note that beep.LoopTimes takes the number of repeats instead of the number of total plays. 48 | func Loop(count int, s StreamSeeker) Streamer { 49 | return &loop{ 50 | s: s, 51 | remains: count, 52 | } 53 | } 54 | 55 | type loop struct { 56 | s StreamSeeker 57 | remains int 58 | } 59 | 60 | func (l *loop) Stream(samples [][2]float64) (n int, ok bool) { 61 | if l.remains == 0 || l.s.Err() != nil { 62 | return 0, false 63 | } 64 | for len(samples) > 0 { 65 | sn, sok := l.s.Stream(samples) 66 | if !sok { 67 | if l.remains > 0 { 68 | l.remains-- 69 | } 70 | if l.remains == 0 { 71 | break 72 | } 73 | err := l.s.Seek(0) 74 | if err != nil { 75 | return n, true 76 | } 77 | continue 78 | } 79 | samples = samples[sn:] 80 | n += sn 81 | } 82 | return n, true 83 | } 84 | 85 | func (l *loop) Err() error { 86 | return l.s.Err() 87 | } 88 | 89 | type LoopOption func(opts *loop2) 90 | 91 | // LoopTimes sets how many times the source stream will repeat. If a section is defined 92 | // by LoopStart, LoopEnd, or LoopBetween, only that section will repeat. 93 | // A value of 0 plays the stream or section once (no repetition); 1 plays it twice, and so on. 94 | func LoopTimes(times int) LoopOption { 95 | if times < 0 { 96 | panic("invalid argument to LoopTimes; times cannot be negative") 97 | } 98 | return func(loop *loop2) { 99 | loop.remains = times 100 | } 101 | } 102 | 103 | // LoopStart sets the position in the source stream to which it returns (using Seek()) 104 | // after reaching the end of the stream or the position set using LoopEnd. The samples 105 | // before this position are played once before the loop begins. 106 | func LoopStart(pos int) LoopOption { 107 | if pos < 0 { 108 | panic("invalid argument to LoopStart; pos cannot be negative") 109 | } 110 | return func(loop *loop2) { 111 | loop.start = pos 112 | } 113 | } 114 | 115 | // LoopEnd sets the position (exclusive) in the source stream up to which the stream plays 116 | // before returning (seeking) back to the start of the stream or the position set by LoopStart. 117 | // The samples after this position are played once after looping completes. 118 | func LoopEnd(pos int) LoopOption { 119 | if pos < 0 { 120 | panic("invalid argument to LoopEnd; pos cannot be negative") 121 | } 122 | return func(loop *loop2) { 123 | loop.end = pos 124 | } 125 | } 126 | 127 | // LoopBetween sets both the LoopStart and LoopEnd positions simultaneously, specifying 128 | // the section of the stream that will be looped. 129 | func LoopBetween(start, end int) LoopOption { 130 | return func(opts *loop2) { 131 | LoopStart(start)(opts) 132 | LoopEnd(end)(opts) 133 | } 134 | } 135 | 136 | // Loop2 takes a StreamSeeker and repeats it according to the specified options. If no LoopTimes 137 | // option is provided, the stream loops indefinitely. LoopStart, LoopEnd, or LoopBetween can define 138 | // a specific section of the stream to loop. Samples before the start and after the end positions 139 | // are played once before and after the looping section, respectively. 140 | // 141 | // The returned Streamer propagates any errors from s. 142 | func Loop2(s StreamSeeker, opts ...LoopOption) (Streamer, error) { 143 | l := &loop2{ 144 | s: s, 145 | remains: -1, // indefinitely 146 | start: 0, 147 | end: math.MaxInt, 148 | } 149 | for _, opt := range opts { 150 | opt(l) 151 | } 152 | 153 | n := s.Len() 154 | if l.start >= n { 155 | return nil, errors.New(fmt.Sprintf("invalid argument to Loop2; start position %d must be smaller than the source streamer length %d", l.start, n)) 156 | } 157 | if l.start >= l.end { 158 | return nil, errors.New(fmt.Sprintf("invalid argument to Loop2; start position %d must be smaller than the end position %d", l.start, l.end)) 159 | } 160 | l.end = min(l.end, n) 161 | 162 | return l, nil 163 | } 164 | 165 | type loop2 struct { 166 | s StreamSeeker 167 | remains int // number of seeks remaining. 168 | start int // start position in the stream where looping begins. Samples before this position are played once before the first loop. 169 | end int // end position in the stream where looping ends and restarts from `start`. 170 | err error 171 | } 172 | 173 | func (l *loop2) Stream(samples [][2]float64) (n int, ok bool) { 174 | if l.err != nil { 175 | return 0, false 176 | } 177 | for len(samples) > 0 { 178 | toStream := len(samples) 179 | if l.remains != 0 { 180 | samplesUntilEnd := l.end - l.s.Position() 181 | if samplesUntilEnd <= 0 { 182 | // End of loop, reset the position and decrease the loop count. 183 | if l.remains > 0 { 184 | l.remains-- 185 | } 186 | if err := l.s.Seek(l.start); err != nil { 187 | l.err = err 188 | return n, true 189 | } 190 | continue 191 | } 192 | // Stream only up to the end of the loop. 193 | toStream = min(samplesUntilEnd, toStream) 194 | } 195 | 196 | sn, sok := l.s.Stream(samples[:toStream]) 197 | n += sn 198 | if sn < toStream || !sok { 199 | l.err = l.s.Err() 200 | return n, n > 0 201 | } 202 | samples = samples[sn:] 203 | } 204 | return n, true 205 | } 206 | 207 | func (l *loop2) Err() error { 208 | return l.err 209 | } 210 | 211 | // Seq takes zero or more Streamers and returns a Streamer which streams them one by one without pauses. 212 | // 213 | // Seq does not propagate errors from the Streamers. 214 | func Seq(s ...Streamer) Streamer { 215 | i := 0 216 | return StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 217 | for i < len(s) && len(samples) > 0 { 218 | sn, sok := s[i].Stream(samples) 219 | samples = samples[sn:] 220 | n, ok = n+sn, ok || sok 221 | if !sok { 222 | i++ 223 | } 224 | } 225 | return n, ok 226 | }) 227 | } 228 | 229 | // Mix takes zero or more Streamers and returns a Streamer which streams them mixed together. 230 | // 231 | // Mix does not propagate errors from the Streamers. 232 | func Mix(s ...Streamer) Streamer { 233 | return &Mixer{ 234 | streamers: s, 235 | stopWhenEmpty: true, 236 | } 237 | } 238 | 239 | // Dup returns two Streamers which both stream the same data as the original s. The two Streamers 240 | // can't be used concurrently without synchronization. 241 | func Dup(s Streamer) (t, u Streamer) { 242 | var tBuf, uBuf [][2]float64 243 | return &dup{&tBuf, &uBuf, s}, &dup{&uBuf, &tBuf, s} 244 | } 245 | 246 | type dup struct { 247 | myBuf, itsBuf *[][2]float64 248 | s Streamer 249 | } 250 | 251 | func (d *dup) Stream(samples [][2]float64) (n int, ok bool) { 252 | buf := *d.myBuf 253 | n = copy(samples, buf) 254 | ok = len(buf) > 0 255 | buf = buf[n:] 256 | samples = samples[n:] 257 | *d.myBuf = buf 258 | 259 | if len(samples) > 0 { 260 | sn, sok := d.s.Stream(samples) 261 | n += sn 262 | ok = ok || sok 263 | *d.itsBuf = append(*d.itsBuf, samples[:sn]...) 264 | } 265 | 266 | return n, ok 267 | } 268 | 269 | func (d *dup) Err() error { 270 | return d.s.Err() 271 | } 272 | -------------------------------------------------------------------------------- /compositors_test.go: -------------------------------------------------------------------------------- 1 | package beep_test 2 | 3 | import ( 4 | "errors" 5 | "math/rand" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | 11 | "github.com/gopxl/beep/v2" 12 | "github.com/gopxl/beep/v2/internal/testtools" 13 | ) 14 | 15 | func TestTake(t *testing.T) { 16 | for i := 0; i < 7; i++ { 17 | total := rand.Intn(1e5) + 1e4 18 | s, data := testtools.RandomDataStreamer(total) 19 | take := rand.Intn(total) 20 | 21 | want := data[:take] 22 | got := testtools.Collect(beep.Take(take, s)) 23 | 24 | if !reflect.DeepEqual(want, got) { 25 | t.Error("Take not working correctly") 26 | } 27 | } 28 | } 29 | 30 | func TestLoop(t *testing.T) { 31 | for i := 0; i < 7; i++ { 32 | for n := 0; n < 5; n++ { 33 | s, data := testtools.RandomDataStreamer(10) 34 | 35 | var want [][2]float64 36 | for j := 0; j < n; j++ { 37 | want = append(want, data...) 38 | } 39 | got := testtools.Collect(beep.Loop(n, s)) 40 | 41 | if !reflect.DeepEqual(want, got) { 42 | t.Error("Loop not working correctly") 43 | } 44 | } 45 | } 46 | } 47 | 48 | func TestLoop2(t *testing.T) { 49 | // LoopStart is bigger than s.Len() 50 | s, _ := testtools.NewSequentialDataStreamer(5) 51 | l, err := beep.Loop2(s, beep.LoopStart(5)) 52 | assert.EqualError(t, err, "invalid argument to Loop2; start position 5 must be smaller than the source streamer length 5") 53 | 54 | // LoopStart is bigger than LoopEnd 55 | s, _ = testtools.NewSequentialDataStreamer(5) 56 | l, err = beep.Loop2(s, beep.LoopBetween(4, 4)) 57 | assert.EqualError(t, err, "invalid argument to Loop2; start position 4 must be smaller than the end position 4") 58 | 59 | // Loop indefinitely (no options). 60 | s, _ = testtools.NewSequentialDataStreamer(5) 61 | l, err = beep.Loop2(s) 62 | assert.NoError(t, err) 63 | got := testtools.CollectNum(16, l) 64 | assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {0, 0}}, got) 65 | 66 | // Test no loop. 67 | s, _ = testtools.NewSequentialDataStreamer(5) 68 | l, err = beep.Loop2(s, beep.LoopTimes(0)) 69 | assert.NoError(t, err) 70 | got = testtools.Collect(l) 71 | assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}, got) 72 | 73 | // Test loop once. 74 | s, _ = testtools.NewSequentialDataStreamer(5) 75 | l, err = beep.Loop2(s, beep.LoopTimes(1)) 76 | assert.NoError(t, err) 77 | got = testtools.Collect(l) 78 | assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}, got) 79 | 80 | // Test loop twice. 81 | s, _ = testtools.NewSequentialDataStreamer(5) 82 | l, err = beep.Loop2(s, beep.LoopTimes(2)) 83 | assert.NoError(t, err) 84 | got = testtools.Collect(l) 85 | assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}, got) 86 | 87 | // Test loop from start position. 88 | s, _ = testtools.NewSequentialDataStreamer(5) 89 | l, err = beep.Loop2(s, beep.LoopTimes(2), beep.LoopStart(2)) 90 | assert.NoError(t, err) 91 | got = testtools.Collect(l) 92 | assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}, {2, 2}, {3, 3}, {4, 4}, {2, 2}, {3, 3}, {4, 4}}, got) 93 | 94 | // Test loop with end position. 95 | s, _ = testtools.NewSequentialDataStreamer(5) 96 | l, err = beep.Loop2(s, beep.LoopTimes(2), beep.LoopEnd(4)) 97 | assert.NoError(t, err) 98 | got = testtools.Collect(l) 99 | assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {0, 0}, {1, 1}, {2, 2}, {3, 3}, {4, 4}}, got) 100 | 101 | // Test loop with start and end position. 102 | s, _ = testtools.NewSequentialDataStreamer(5) 103 | l, err = beep.Loop2(s, beep.LoopTimes(2), beep.LoopBetween(2, 4)) 104 | assert.NoError(t, err) 105 | got = testtools.Collect(l) 106 | assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {2, 2}, {3, 3}, {2, 2}, {3, 3}, {4, 4}}, got) 107 | 108 | // Loop indefinitely with both start and end position. 109 | s, _ = testtools.NewSequentialDataStreamer(5) 110 | l, err = beep.Loop2(s, beep.LoopBetween(2, 4)) 111 | assert.NoError(t, err) 112 | got = testtools.CollectNum(10, l) 113 | assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {2, 2}, {3, 3}, {2, 2}, {3, 3}, {2, 2}, {3, 3}}, got) 114 | 115 | //// Test streaming from the middle of the loops. 116 | s, _ = testtools.NewSequentialDataStreamer(5) 117 | l, err = beep.Loop2(s, beep.LoopTimes(2), beep.LoopBetween(2, 4)) // 0, 1, 2, 3, 2, 3, 2, 3 118 | assert.NoError(t, err) 119 | // First stream to the middle of a loop. 120 | buf := make([][2]float64, 3) 121 | if n, ok := l.Stream(buf); n != 3 || !ok { 122 | t.Fatalf("want n %d got %d, want ok %t got %t", 3, n, true, ok) 123 | } 124 | assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}}, buf) 125 | // Then stream starting at the middle of the loop. 126 | if n, ok := l.Stream(buf); n != 3 || !ok { 127 | t.Fatalf("want n %d got %d, want ok %t got %t", 3, n, true, ok) 128 | } 129 | assert.Equal(t, [][2]float64{{3, 3}, {2, 2}, {3, 3}}, buf) 130 | 131 | // Test error handling in middle of loop. 132 | expectedErr := errors.New("expected error") 133 | s, _ = testtools.NewSequentialDataStreamer(5) 134 | s = testtools.NewDelayedErrorStreamer(s, 5, expectedErr) 135 | l, err = beep.Loop2(s, beep.LoopTimes(3), beep.LoopBetween(2, 4)) // 0, 1, 2, 3, 2, 3, 2, 3 136 | assert.NoError(t, err) 137 | buf = make([][2]float64, 10) 138 | if n, ok := l.Stream(buf); n != 5 || !ok { 139 | t.Fatalf("want n %d got %d, want ok %t got %t", 5, n, true, ok) 140 | } 141 | assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {2, 2}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}}, buf) 142 | assert.Equal(t, expectedErr, l.Err()) 143 | if n, ok := l.Stream(buf); n != 0 || ok { 144 | t.Fatalf("want n %d got %d, want ok %t got %t", 0, n, false, ok) 145 | } 146 | assert.Equal(t, expectedErr, l.Err()) 147 | 148 | // Test error handling during call to Seek(). 149 | s, _ = testtools.NewSequentialDataStreamer(5) 150 | s = testtools.NewSeekErrorStreamer(s, expectedErr) 151 | l, err = beep.Loop2(s, beep.LoopTimes(3), beep.LoopBetween(2, 4)) // 0, 1, 2, 3, [error] 152 | assert.NoError(t, err) 153 | buf = make([][2]float64, 10) 154 | if n, ok := l.Stream(buf); n != 4 || !ok { 155 | t.Fatalf("want n %d got %d, want ok %t got %t", 4, n, true, ok) 156 | } 157 | assert.Equal(t, [][2]float64{{0, 0}, {1, 1}, {2, 2}, {3, 3}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}}, buf) 158 | assert.Equal(t, expectedErr, l.Err()) 159 | if n, ok := l.Stream(buf); n != 0 || ok { 160 | t.Fatalf("want n %d got %d, want ok %t got %t", 0, n, false, ok) 161 | } 162 | assert.Equal(t, expectedErr, l.Err()) 163 | } 164 | 165 | func TestSeq(t *testing.T) { 166 | var ( 167 | n = 7 168 | s = make([]beep.Streamer, n) 169 | data = make([][][2]float64, n) 170 | ) 171 | for i := range s { 172 | s[i], data[i] = testtools.RandomDataStreamer(rand.Intn(1e5) + 1e4) 173 | } 174 | 175 | var want [][2]float64 176 | for _, d := range data { 177 | want = append(want, d...) 178 | } 179 | 180 | got := testtools.Collect(beep.Seq(s...)) 181 | 182 | if !reflect.DeepEqual(want, got) { 183 | t.Errorf("Seq not working properly") 184 | } 185 | } 186 | 187 | func TestMix(t *testing.T) { 188 | var ( 189 | n = 7 190 | s = make([]beep.Streamer, n) 191 | data = make([][][2]float64, n) 192 | ) 193 | for i := range s { 194 | s[i], data[i] = testtools.RandomDataStreamer(rand.Intn(1e5) + 1e4) 195 | } 196 | 197 | maxLen := 0 198 | for _, d := range data { 199 | maxLen = max(maxLen, len(d)) 200 | } 201 | 202 | want := make([][2]float64, maxLen) 203 | for _, d := range data { 204 | for i := range d { 205 | want[i][0] += d[i][0] 206 | want[i][1] += d[i][1] 207 | } 208 | } 209 | 210 | got := testtools.Collect(beep.Mix(s...)) 211 | 212 | testtools.AssertSamplesEqual(t, want, got) 213 | } 214 | 215 | func TestDup(t *testing.T) { 216 | for i := 0; i < 7; i++ { 217 | s, data := testtools.RandomDataStreamer(rand.Intn(1e5) + 1e4) 218 | st, su := beep.Dup(s) 219 | 220 | var tData, uData [][2]float64 221 | for { 222 | buf := make([][2]float64, rand.Intn(1e4)) 223 | tn, tok := st.Stream(buf) 224 | tData = append(tData, buf[:tn]...) 225 | 226 | buf = make([][2]float64, rand.Intn(1e4)) 227 | un, uok := su.Stream(buf) 228 | uData = append(uData, buf[:un]...) 229 | 230 | if !tok && !uok { 231 | break 232 | } 233 | } 234 | 235 | if !reflect.DeepEqual(data, tData) || !reflect.DeepEqual(data, uData) { 236 | t.Error("Dup not working correctly") 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /ctrl.go: -------------------------------------------------------------------------------- 1 | package beep 2 | 3 | // Ctrl allows for pausing a Streamer. 4 | // 5 | // Wrap a Streamer in a Ctrl. 6 | // 7 | // ctrl := &beep.Ctrl{Streamer: s} 8 | // 9 | // Then, we can pause the streaming (this will cause Ctrl to stream silence). 10 | // 11 | // ctrl.Paused = true 12 | // 13 | // To completely stop a Ctrl before the wrapped Streamer is drained, just set the wrapped Streamer 14 | // to nil. 15 | // 16 | // ctrl.Streamer = nil 17 | // 18 | // If you're playing a Streamer wrapped in a Ctrl through the speaker, you need to lock and unlock 19 | // the speaker when modifying the Ctrl to avoid race conditions. 20 | // 21 | // speaker.Play(ctrl) 22 | // // ... 23 | // speaker.Lock() 24 | // ctrl.Paused = true 25 | // speaker.Unlock() 26 | type Ctrl struct { 27 | Streamer Streamer 28 | Paused bool 29 | } 30 | 31 | // Stream streams the wrapped Streamer, if not nil. If the Streamer is nil, Ctrl acts as drained. 32 | // When paused, Ctrl streams silence. 33 | func (c *Ctrl) Stream(samples [][2]float64) (n int, ok bool) { 34 | if c.Streamer == nil { 35 | return 0, false 36 | } 37 | if c.Paused { 38 | clear(samples) 39 | return len(samples), true 40 | } 41 | return c.Streamer.Stream(samples) 42 | } 43 | 44 | // Err returns the error of the wrapped Streamer, if not nil. 45 | func (c *Ctrl) Err() error { 46 | if c.Streamer == nil { 47 | return nil 48 | } 49 | return c.Streamer.Err() 50 | } 51 | -------------------------------------------------------------------------------- /ctrl_test.go: -------------------------------------------------------------------------------- 1 | package beep_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/gopxl/beep/v2" 10 | "github.com/gopxl/beep/v2/internal/testtools" 11 | ) 12 | 13 | func TestCtrl_CanBePausedAndUnpaused(t *testing.T) { 14 | s, data := testtools.RandomDataStreamer(20) 15 | 16 | ctrl := beep.Ctrl{ 17 | Streamer: s, 18 | Paused: false, 19 | } 20 | 21 | got := testtools.CollectNum(10, &ctrl) 22 | assert.Equal(t, data[:10], got) 23 | 24 | ctrl.Paused = true 25 | got = testtools.CollectNum(10, &ctrl) 26 | assert.Equal(t, make([][2]float64, 10), got) 27 | 28 | ctrl.Paused = false 29 | got = testtools.CollectNum(10, &ctrl) 30 | assert.Equal(t, data[10:20], got) 31 | } 32 | 33 | func TestCtrl_DoesNotStreamFromNilStreamer(t *testing.T) { 34 | ctrl := beep.Ctrl{ 35 | Streamer: nil, 36 | Paused: false, 37 | } 38 | 39 | buf := make([][2]float64, 10) 40 | n, ok := ctrl.Stream(buf) 41 | assert.Equal(t, 0, n) 42 | assert.False(t, ok) 43 | } 44 | 45 | func TestCtrl_PropagatesErrors(t *testing.T) { 46 | ctrl := beep.Ctrl{} 47 | 48 | assert.NoError(t, ctrl.Err()) 49 | 50 | err := errors.New("oh no") 51 | ctrl.Streamer = testtools.NewErrorStreamer(err) 52 | assert.Equal(t, err, ctrl.Err()) 53 | } 54 | -------------------------------------------------------------------------------- /docs/Tutorial/01. Home.md: -------------------------------------------------------------------------------- 1 | # Beep Tutorial 2 | 3 | Hi! 4 | 5 | **Beep** is a simple, fast, and very flexible little library for playing and manipulating sounds in any Go application. 6 | 7 | Although Beep is easy to use, a tutorial is always helpful. 8 | 9 | Choose the first part of the tutorial from the panel on the right. 10 | 11 | For more details about the specifics of Beep API, visit [GoDoc](https://godoc.org/github.com/gopxl/beep). -------------------------------------------------------------------------------- /docs/Tutorial/02. Hello, Beep!.md: -------------------------------------------------------------------------------- 1 | # Hello, Beep! 2 | 3 | Welcome to the Beep tutorial! In this part, we'll learn how to load a song, initialize the speaker, and wake up your neighbors. 4 | 5 | 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: 6 | 7 | ``` 8 | $ go get -u github.com/gopxl/beep 9 | ``` 10 | 11 | We'll start with a plain main function and we'll import the Beep package: 12 | 13 | ```go 14 | package main 15 | 16 | import "github.com/gopxl/beep" 17 | 18 | func main() { 19 | // here we go! 20 | } 21 | ``` 22 | 23 | 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. 24 | 25 | 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): 26 | 27 | ```go 28 | package main 29 | 30 | import ( 31 | "log" 32 | "os" 33 | 34 | "github.com/gopxl/beep" 35 | ) 36 | 37 | func main() { 38 | f, err := os.Open("Rockafeller Skank.mp3") 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | } 43 | 44 | ``` 45 | 46 | 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. 47 | 48 | ```go 49 | f, err := os.Open("Rockafeller Skank.mp3") 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | streamer, format, err := mp3.Decode(f) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | ``` 59 | 60 | 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). 61 | 62 | > **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.) 63 | 64 | And we'll do exactly that: 65 | 66 | ```go 67 | streamer, format, err := mp3.Decode(f) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | defer streamer.Close() 72 | ``` 73 | 74 | 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. 75 | 76 | > **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. 77 | > 78 | > 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. 79 | 80 | 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. 81 | 82 | Okay, onto waking up the neighbors! 83 | 84 | 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. 85 | 86 | ```go 87 | streamer, format, err := mp3.Decode(f) 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | defer streamer.Close() 92 | 93 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) 94 | ``` 95 | 96 | Whoa, there's a lot going on here! The function `speaker.Init` takes two arguments: the sample rate, and the buffer size. 97 | 98 | **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. 99 | 100 | **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. 101 | 102 | > **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. 103 | 104 | Now, we can finally play the streamer! 105 | 106 | ```go 107 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) 108 | 109 | speaker.Play(streamer) 110 | ``` 111 | 112 | **Let's run it!** 113 | 114 | ``` 115 | $ go run hello-beep.go 116 | $ 117 | ``` 118 | 119 | Nothing? The program just finished immediately. 120 | 121 | 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: 122 | 123 | ```go 124 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) 125 | 126 | speaker.Play(streamer) 127 | select {} 128 | ``` 129 | 130 | **Run it again!** 131 | 132 | ``` 133 | $ go run hello-beep.go 134 | ``` 135 | 136 | Now it works! Perfect. 137 | 138 | > **Note:** You can easily play multiple streamers simultaneously, simply by sending all of them to the speaker with `speaker.Play`. 139 | 140 | 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: 141 | 142 | ```go 143 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) 144 | 145 | done := make(chan bool) 146 | speaker.Play(beep.Seq(streamer, beep.Callback(func() { 147 | done <- true 148 | }))) 149 | 150 | <-done 151 | ``` 152 | 153 | 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! 154 | 155 | > **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. 156 | 157 | When we run the program now, it hangs until the song finishes playing, then quits. Exactly as we intended! 158 | 159 | ## Dealing with different sample rates 160 | 161 | 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. 162 | 163 | **Let's see what happens!** 164 | 165 | Change the speaker initialization to this: 166 | 167 | ```go 168 | sr := format.SampleRate * 2 169 | speaker.Init(sr, sr.N(time.Second/10)) 170 | ``` 171 | 172 | We've doubled the sample rate. 173 | 174 | ``` 175 | $ go run hello-beep.go 176 | ``` 177 | 178 | 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): 179 | 180 | ```go 181 | resampled := beep.Resample(4, format.SampleRate, sr, streamer) 182 | 183 | done := make(chan bool) 184 | speaker.Play(beep.Seq(resampled, beep.Callback(func() { 185 | done <- true 186 | }))) 187 | 188 | <-done 189 | ``` 190 | 191 | 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. 192 | 193 | 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. 194 | 195 | Now it plays in the original speed as before! So, that's how you deal with sample rates. 196 | 197 | 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. 198 | 199 | Alright, that's all for this part, see you in the next one! -------------------------------------------------------------------------------- /docs/Tutorial/04. Making Own Streamers.md: -------------------------------------------------------------------------------- 1 | # Making own streamers 2 | 3 | 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. 4 | 5 | So, what's a streamer? It's this interface: 6 | 7 | ```go 8 | type Streamer interface { 9 | Stream(samples [][2]float64) (n int, ok bool) 10 | Err() error 11 | } 12 | ``` 13 | 14 | Read [the docs](https://godoc.org/github.com/gopxl/beep#Streamer) for more details. 15 | 16 | > **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. 17 | > 18 | > 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. 19 | 20 | To make our very own streamer, all that's needed is implementing that interface. Let's get to it! 21 | 22 | ## Noise generator 23 | 24 | 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: 25 | 26 | ```go 27 | type Noise struct{} 28 | ``` 29 | 30 | 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`. 31 | 32 | The samples are expected to be values between -1 and +1 (including). We fill the slice using a simple for-loop: 33 | 34 | ```go 35 | func (no Noise) Stream(samples [][2]float64) (n int, ok bool) { 36 | for i := range samples { 37 | samples[i][0] = rand.Float64()*2 - 1 38 | samples[i][1] = rand.Float64()*2 - 1 39 | } 40 | return len(samples), true 41 | } 42 | ``` 43 | 44 | The last thing remaining is the `Err` method. The noise generator can never malfunction, so `Err` always returns `nil`: 45 | 46 | ```go 47 | func (no Noise) Err() error { 48 | return nil 49 | } 50 | ``` 51 | 52 | Now it's done and we can use it in a program: 53 | 54 | ```go 55 | func main() { 56 | sr := beep.SampleRate(44100) 57 | speaker.Init(sr, sr.N(time.Second/10)) 58 | speaker.Play(Noise{}) 59 | select {} 60 | } 61 | ``` 62 | 63 | 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): 64 | 65 | ```go 66 | func main() { 67 | sr := beep.SampleRate(44100) 68 | speaker.Init(sr, sr.N(time.Second/10)) 69 | 70 | done := make(chan bool) 71 | speaker.Play(beep.Seq(beep.Take(sr.N(5*time.Second), Noise{}), beep.Callback(func() { 72 | done <- true 73 | }))) 74 | <-done 75 | } 76 | ``` 77 | 78 | This will play noise for 5 seconds. 79 | 80 | 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: 81 | 82 | ```go 83 | type StreamerFunc func(samples [][2]float64) (n int, ok bool) 84 | ``` 85 | 86 | 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: 87 | 88 | ```go 89 | func Noise() beep.Streamer { 90 | return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 91 | for i := range samples { 92 | samples[i][0] = rand.Float64()*2 - 1 93 | samples[i][1] = rand.Float64()*2 - 1 94 | } 95 | return len(samples), true 96 | }) 97 | } 98 | ``` 99 | 100 | 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. 101 | 102 | ## Queue 103 | 104 | Well, that was simple. How about something more complex? 105 | 106 | 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. 107 | 108 | 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. 109 | 110 | Let's get to it! The `Queue` type needs just one thing to remember: the streamers left to play. 111 | 112 | ```go 113 | type Queue struct { 114 | streamers []beep.Streamer 115 | } 116 | ``` 117 | 118 | We need a method to add new streamers to the queue: 119 | 120 | ```go 121 | func (q *Queue) Add(streamers ...beep.Streamer) { 122 | q.streamers = append(q.streamers, streamers...) 123 | } 124 | ``` 125 | 126 | Now, all that's remaining is to implement the streamer interface. Here's the `Stream` method with comments for understanding: 127 | 128 | ```go 129 | func (q *Queue) Stream(samples [][2]float64) (n int, ok bool) { 130 | // We use the filled variable to track how many samples we've 131 | // successfully filled already. We loop until all samples are filled. 132 | filled := 0 133 | for filled < len(samples) { 134 | // There are no streamers in the queue, so we stream silence. 135 | if len(q.streamers) == 0 { 136 | for i := range samples[filled:] { 137 | samples[i][0] = 0 138 | samples[i][1] = 0 139 | } 140 | break 141 | } 142 | 143 | // We stream from the first streamer in the queue. 144 | n, ok := q.streamers[0].Stream(samples[filled:]) 145 | // If it's drained, we pop it from the queue, thus continuing with 146 | // the next streamer. 147 | if !ok { 148 | q.streamers = q.streamers[1:] 149 | } 150 | // We update the number of filled samples. 151 | filled += n 152 | } 153 | return len(samples), true 154 | } 155 | ``` 156 | 157 | And here's the trivial `Err` method: 158 | 159 | ```go 160 | func (q *Queue) Err() error { 161 | return nil 162 | } 163 | ``` 164 | 165 | 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. 166 | 167 | Here's how it's done (again, with comments): 168 | 169 | ```go 170 | func main() { 171 | sr := beep.SampleRate(44100) 172 | speaker.Init(sr, sr.N(time.Second/10)) 173 | 174 | // A zero Queue is an empty Queue. 175 | var queue Queue 176 | speaker.Play(&queue) 177 | 178 | for { 179 | var name string 180 | fmt.Print("Type an MP3 file name: ") 181 | fmt.Scanln(&name) 182 | 183 | // Open the file on the disk. 184 | f, err := os.Open(name) 185 | if err != nil { 186 | fmt.Println(err) 187 | continue 188 | } 189 | 190 | // Decode it. 191 | streamer, format, err := mp3.Decode(f) 192 | if err != nil { 193 | fmt.Println(err) 194 | continue 195 | } 196 | 197 | // The speaker's sample rate is fixed at 44100. Therefore, we need to 198 | // resample the file in case it's in a different sample rate. 199 | resampled := beep.Resample(4, format.SampleRate, sr, streamer) 200 | 201 | // And finally, we add the song to the queue. 202 | speaker.Lock() 203 | queue.Add(resampled) 204 | speaker.Unlock() 205 | } 206 | } 207 | ``` 208 | 209 | And that's it! 210 | 211 | 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!_ 212 | 213 | > **Why isn't the `Queue` type implemented in Beep?** So that I could make this tutorial. -------------------------------------------------------------------------------- /docs/Tutorial/04. To Buffer, or Not To Buffer.md: -------------------------------------------------------------------------------- 1 | # To buffer, or not to buffer, that is the question 2 | 3 | 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. 4 | 5 | In this part, we'll learn how to load a sound to memory and then stream it from there. 6 | 7 | **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: 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "log" 14 | "os" 15 | 16 | "github.com/gopxl/beep/mp3" 17 | ) 18 | 19 | func main() { 20 | f, err := os.Open("gunshot.mp3") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | streamer, format, err := mp3.Decode(f) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) 31 | 32 | // TODO 33 | } 34 | ``` 35 | 36 | Notice that we omitted the `defer streamer.Close()` line. That's because we actually will be closing the streamer before finishing the program. 37 | 38 | 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. 39 | 40 | First, we need to create a buffer (we don't really need to initialize the speaker before making the buffer): 41 | 42 | ```go 43 | streamer, format, err := mp3.Decode(f) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) 49 | 50 | buffer := beep.NewBuffer(format) 51 | ``` 52 | 53 | 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. 54 | 55 | We'll use the same format as the loaded audio file. That way we won't lose any quality. 56 | 57 | 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). 58 | 59 | ```go 60 | buffer := beep.NewBuffer(format) 61 | buffer.Append(streamer) 62 | ``` 63 | 64 | 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. 65 | 66 | At this point, `streamer` is drained and no longer needed. We can close it (that closes the source file as well): 67 | 68 | ```go 69 | buffer := beep.NewBuffer(format) 70 | buffer.Append(streamer) 71 | streamer.Close() 72 | ``` 73 | 74 | 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. 75 | 76 | > **Note:** Because it returns a `beep.StreamSeeker`, we can loop it and rewind it as we like. 77 | 78 | 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. 79 | 80 | So, let's do that! Let's make it so that entering a newline will fire a gunshot! That should be easy to do: 81 | 82 | ```go 83 | buffer := beep.NewBuffer(format) 84 | buffer.Append(streamer) 85 | streamer.Close() 86 | 87 | for { 88 | fmt.Print("Press [ENTER] to fire a gunshot! ") 89 | fmt.Scanln() 90 | 91 | shot := buffer.Streamer(0, buffer.Len()) 92 | speaker.Play(shot) 93 | } 94 | ``` 95 | 96 | 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! 97 | 98 | > **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`. 99 | 100 | ## When to buffer and when not to? 101 | 102 | 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. 103 | 104 | 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. -------------------------------------------------------------------------------- /effects/doc.go: -------------------------------------------------------------------------------- 1 | // Package effects provides additional audio effects for the Beep library. 2 | package effects 3 | -------------------------------------------------------------------------------- /effects/doppler.go: -------------------------------------------------------------------------------- 1 | package effects 2 | 3 | import "github.com/gopxl/beep/v2" 4 | 5 | // Doppler simulates a "sound at a distance". If the sound starts at a far distance, 6 | // it'll take some time to reach the ears of the listener. 7 | // 8 | // The distance of the sound can change dynamically. Doppler adjusts the density of 9 | // the sound (affecting its speed) to remain consistent with the distance. This is called 10 | // the Doppler effect. 11 | // 12 | // The arguments are: 13 | // 14 | // quality: the quality of the underlying resampler (1 or 2 is usually okay) 15 | // samplesPerMeter: sample rate / speed of sound 16 | // s: the source streamer 17 | // distance: a function to calculate the current distance; takes number of 18 | // samples Doppler wants to stream at the moment 19 | // 20 | // This function is experimental and may change any time! 21 | func Doppler(quality int, samplesPerMeter float64, s beep.Streamer, distance func(delta int) float64) beep.Streamer { 22 | return &doppler{ 23 | r: beep.ResampleRatio(quality, 1, s), 24 | distance: distance, 25 | space: make([][2]float64, int(distance(0)*samplesPerMeter)), 26 | samplesPerMeter: samplesPerMeter, 27 | } 28 | } 29 | 30 | type doppler struct { 31 | r *beep.Resampler 32 | distance func(delta int) float64 33 | space [][2]float64 34 | samplesPerMeter float64 35 | } 36 | 37 | func (d *doppler) Stream(samples [][2]float64) (n int, ok bool) { 38 | distance := d.distance(len(samples)) 39 | currentSpaceLen := int(distance * d.samplesPerMeter) 40 | difference := currentSpaceLen - len(d.space) 41 | 42 | d.r.SetRatio(float64(len(samples)) / float64(len(samples)+difference)) 43 | 44 | d.space = append(d.space, make([][2]float64, len(samples)+difference)...) 45 | rn, _ := d.r.Stream(d.space[len(d.space)-len(samples)-difference:]) 46 | d.space = d.space[:len(d.space)-len(samples)-difference+rn] 47 | for i := len(d.space) - rn; i < len(d.space); i++ { 48 | d.space[i][0] /= distance * distance 49 | d.space[i][1] /= distance * distance 50 | } 51 | 52 | if len(d.space) == 0 { 53 | return 0, false 54 | } 55 | n = copy(samples, d.space) 56 | d.space = d.space[n:] 57 | return n, true 58 | } 59 | 60 | func (d *doppler) Err() error { 61 | return d.r.Err() 62 | } 63 | -------------------------------------------------------------------------------- /effects/equalizer.go: -------------------------------------------------------------------------------- 1 | package effects 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/gopxl/beep/v2" 7 | ) 8 | 9 | type ( 10 | 11 | // This parametric equalizer is based on the GK Nilsen's post at: 12 | // https://octovoid.com/2017/11/04/coding-a-parametric-equalizer-for-audio-applications/ 13 | equalizer struct { 14 | streamer beep.Streamer 15 | sections []section 16 | } 17 | 18 | section struct { 19 | a, b [2][]float64 20 | xPast, yPast [][2]float64 21 | } 22 | 23 | // EqualizerSections is the interfacd that is passed into NewEqualizer 24 | EqualizerSections interface { 25 | sections(fs float64) []section 26 | } 27 | 28 | StereoEqualizerSection struct { 29 | Left MonoEqualizerSection 30 | Right MonoEqualizerSection 31 | } 32 | 33 | MonoEqualizerSection struct { 34 | // F0 (center frequency) sets the mid-point of the section’s 35 | // frequency range and is given in Hertz [Hz]. 36 | F0 float64 37 | 38 | // Bf (bandwidth) represents the width of the section across 39 | // frequency and is measured in Hertz [Hz]. A low bandwidth 40 | // corresponds to a narrow frequency range meaning that the 41 | // section will concentrate its operation to only the 42 | // frequencies close to the center frequency. On the other hand, 43 | // a high bandwidth yields a section of wide frequency range — 44 | // affecting a broader range of frequencies surrounding the 45 | // center frequency. 46 | Bf float64 47 | 48 | // GB (bandwidth gain) is given in decibels [dB] and represents 49 | // the level at which the bandwidth is measured. That is, to 50 | // have a meaningful measure of bandwidth, we must define the 51 | // level at which it is measured. 52 | GB float64 53 | 54 | // G0 (reference gain) is given in decibels [dB] and simply 55 | // represents the level of the section’s offset. 56 | G0 float64 57 | 58 | // G (boost/cut gain) is given in decibels [dB] and prescribes 59 | // the effect imposed on the audio loudness for the section’s 60 | // frequency range. A boost/cut level of 0 dB corresponds to 61 | // unity (no operation), whereas negative numbers corresponds to 62 | // cut (volume down) and positive numbers to boost (volume up). 63 | G float64 64 | } 65 | 66 | // StereoEqualizerSections implements EqualizerSections and can be passed into NewEqualizer 67 | StereoEqualizerSections []StereoEqualizerSection 68 | 69 | // MonoEqualizerSections implements EqualizerSections and can be passed into NewEqualizer 70 | MonoEqualizerSections []MonoEqualizerSection 71 | ) 72 | 73 | // NewEqualizer returns a beep.Streamer that modifies the stream based on the EqualizerSection slice that is passed in. 74 | // The SampleRate (sr) must match that of the Streamer. 75 | func NewEqualizer(st beep.Streamer, sr beep.SampleRate, s EqualizerSections) beep.Streamer { 76 | return &equalizer{ 77 | streamer: st, 78 | sections: s.sections(float64(sr)), 79 | } 80 | } 81 | 82 | func (m MonoEqualizerSections) sections(fs float64) []section { 83 | out := make([]section, len(m)) 84 | for i, s := range m { 85 | out[i] = s.section(fs) 86 | } 87 | return out 88 | } 89 | 90 | func (m StereoEqualizerSections) sections(fs float64) []section { 91 | out := make([]section, len(m)) 92 | for i, s := range m { 93 | out[i] = s.section(fs) 94 | } 95 | return out 96 | } 97 | 98 | // Stream streams the wrapped Streamer modified by Equalizer. 99 | func (e *equalizer) Stream(samples [][2]float64) (n int, ok bool) { 100 | n, ok = e.streamer.Stream(samples) 101 | for _, s := range e.sections { 102 | s.apply(samples) 103 | } 104 | return n, ok 105 | } 106 | 107 | // Err propagates the wrapped Streamer's errors. 108 | func (e *equalizer) Err() error { 109 | return e.streamer.Err() 110 | } 111 | 112 | func (m MonoEqualizerSection) section(fs float64) section { 113 | beta := math.Tan(m.Bf/2.0*math.Pi/(fs/2.0)) * 114 | math.Sqrt(math.Abs(math.Pow(math.Pow(10, m.GB/20.0), 2.0)- 115 | math.Pow(math.Pow(10.0, m.G0/20.0), 2.0))) / 116 | math.Sqrt(math.Abs(math.Pow(math.Pow(10.0, m.G/20.0), 2.0)- 117 | math.Pow(math.Pow(10.0, m.GB/20.0), 2.0))) 118 | 119 | b := []float64{ 120 | (math.Pow(10.0, m.G0/20.0) + math.Pow(10.0, m.G/20.0)*beta) / (1 + beta), 121 | (-2 * math.Pow(10.0, m.G0/20.0) * math.Cos(m.F0*math.Pi/(fs/2.0))) / (1 + beta), 122 | (math.Pow(10.0, m.G0/20) - math.Pow(10.0, m.G/20.0)*beta) / (1 + beta), 123 | } 124 | 125 | a := []float64{ 126 | 1.0, 127 | -2 * math.Cos(m.F0*math.Pi/(fs/2.0)) / (1 + beta), 128 | (1 - beta) / (1 + beta), 129 | } 130 | 131 | return section{ 132 | a: [2][]float64{a, a}, 133 | b: [2][]float64{b, b}, 134 | } 135 | } 136 | 137 | func (s StereoEqualizerSection) section(fs float64) section { 138 | l := s.Left.section(fs) 139 | r := s.Right.section(fs) 140 | 141 | return section{ 142 | a: [2][]float64{l.a[0], r.a[0]}, 143 | b: [2][]float64{l.b[0], r.b[0]}, 144 | } 145 | } 146 | 147 | func (s *section) apply(x [][2]float64) { 148 | ord := len(s.a[0]) - 1 149 | np := len(x) - 1 150 | 151 | if np < ord { 152 | x = append(x, make([][2]float64, ord-np)...) 153 | np = ord 154 | } 155 | 156 | y := make([][2]float64, len(x)) 157 | 158 | if len(s.xPast) < len(x) { 159 | s.xPast = append(s.xPast, make([][2]float64, len(x)-len(s.xPast))...) 160 | } 161 | 162 | if len(s.yPast) < len(x) { 163 | s.yPast = append(s.yPast, make([][2]float64, len(x)-len(s.yPast))...) 164 | } 165 | 166 | for i := 0; i < len(x); i++ { 167 | for j := 0; j < ord+1; j++ { 168 | if i-j < 0 { 169 | y[i][0] = y[i][0] + s.b[0][j]*s.xPast[len(s.xPast)-j][0] 170 | y[i][1] = y[i][1] + s.b[1][j]*s.xPast[len(s.xPast)-j][1] 171 | } else { 172 | y[i][0] = y[i][0] + s.b[0][j]*x[i-j][0] 173 | y[i][1] = y[i][1] + s.b[1][j]*x[i-j][1] 174 | } 175 | } 176 | 177 | for j := 0; j < ord; j++ { 178 | if i-j-1 < 0 { 179 | y[i][0] = y[i][0] - s.a[0][j+1]*s.yPast[len(s.yPast)-j-1][0] 180 | y[i][1] = y[i][1] - s.a[1][j+1]*s.yPast[len(s.yPast)-j-1][1] 181 | } else { 182 | y[i][0] = y[i][0] - s.a[0][j+1]*y[i-j-1][0] 183 | y[i][1] = y[i][1] - s.a[1][j+1]*y[i-j-1][1] 184 | } 185 | } 186 | 187 | y[i][0] = y[i][0] / s.a[0][0] 188 | y[i][1] = y[i][1] / s.a[1][0] 189 | } 190 | 191 | s.xPast = x[:] 192 | s.yPast = y[:] 193 | copy(x, y) 194 | } 195 | -------------------------------------------------------------------------------- /effects/gain.go: -------------------------------------------------------------------------------- 1 | package effects 2 | 3 | import "github.com/gopxl/beep/v2" 4 | 5 | // Gain amplifies the wrapped Streamer. The output of the wrapped Streamer gets multiplied by 6 | // 1+Gain. 7 | // 8 | // Note that gain is not equivalent to the human perception of volume. Human perception of volume is 9 | // roughly exponential, while gain only amplifies linearly. 10 | type Gain struct { 11 | Streamer beep.Streamer 12 | Gain float64 13 | } 14 | 15 | // Stream streams the wrapped Streamer amplified by Gain. 16 | func (g *Gain) Stream(samples [][2]float64) (n int, ok bool) { 17 | n, ok = g.Streamer.Stream(samples) 18 | for i := range samples[:n] { 19 | samples[i][0] *= 1 + g.Gain 20 | samples[i][1] *= 1 + g.Gain 21 | } 22 | return n, ok 23 | } 24 | 25 | // Err propagates the wrapped Streamer's errors. 26 | func (g *Gain) Err() error { 27 | return g.Streamer.Err() 28 | } 29 | -------------------------------------------------------------------------------- /effects/mono.go: -------------------------------------------------------------------------------- 1 | package effects 2 | 3 | import "github.com/gopxl/beep/v2" 4 | 5 | // Mono converts the wrapped Streamer to a mono buffer 6 | // by downmixing the left and right channels together. 7 | // 8 | // The returned Streamer propagates s's errors through Err. 9 | func Mono(s beep.Streamer) beep.Streamer { 10 | return &mono{s} 11 | } 12 | 13 | type mono struct { 14 | Streamer beep.Streamer 15 | } 16 | 17 | func (m *mono) Stream(samples [][2]float64) (n int, ok bool) { 18 | n, ok = m.Streamer.Stream(samples) 19 | for i := range samples[:n] { 20 | mix := (samples[i][0] + samples[i][1]) / 2 21 | samples[i][0], samples[i][1] = mix, mix 22 | } 23 | return n, ok 24 | } 25 | 26 | func (m *mono) Err() error { 27 | return m.Streamer.Err() 28 | } 29 | -------------------------------------------------------------------------------- /effects/pan.go: -------------------------------------------------------------------------------- 1 | package effects 2 | 3 | import "github.com/gopxl/beep/v2" 4 | 5 | // Pan balances the wrapped Streamer between the left and the right channel. The Pan field value of 6 | // -1 means that both original channels go through the left channel. The value of +1 means the same 7 | // for the right channel. The value of 0 changes nothing. 8 | type Pan struct { 9 | Streamer beep.Streamer 10 | Pan float64 11 | } 12 | 13 | // Stream streams the wrapped Streamer balanced by Pan. 14 | func (p *Pan) Stream(samples [][2]float64) (n int, ok bool) { 15 | n, ok = p.Streamer.Stream(samples) 16 | switch { 17 | case p.Pan < 0: 18 | for i := range samples[:n] { 19 | r := samples[i][1] 20 | samples[i][0] += -p.Pan * r 21 | samples[i][1] -= -p.Pan * r 22 | } 23 | case p.Pan > 0: 24 | for i := range samples[:n] { 25 | l := samples[i][0] 26 | samples[i][0] -= p.Pan * l 27 | samples[i][1] += p.Pan * l 28 | } 29 | } 30 | return n, ok 31 | } 32 | 33 | // Err propagates the wrapped Streamer's errors. 34 | func (p *Pan) Err() error { 35 | return p.Streamer.Err() 36 | } 37 | -------------------------------------------------------------------------------- /effects/swap.go: -------------------------------------------------------------------------------- 1 | package effects 2 | 3 | import "github.com/gopxl/beep/v2" 4 | 5 | // Swap swaps the left and right channel of the wrapped Streamer. 6 | // 7 | // The returned Streamer propagates s's errors through Err. 8 | func Swap(s beep.Streamer) beep.Streamer { 9 | return &swap{s} 10 | } 11 | 12 | type swap struct { 13 | Streamer beep.Streamer 14 | } 15 | 16 | func (s *swap) Stream(samples [][2]float64) (n int, ok bool) { 17 | n, ok = s.Streamer.Stream(samples) 18 | for i := range samples[:n] { 19 | samples[i][0], samples[i][1] = samples[i][1], samples[i][0] 20 | } 21 | return n, ok 22 | } 23 | 24 | func (s *swap) Err() error { 25 | return s.Streamer.Err() 26 | } 27 | -------------------------------------------------------------------------------- /effects/transition.go: -------------------------------------------------------------------------------- 1 | package effects 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/gopxl/beep/v2" 7 | ) 8 | 9 | // TransitionFunc defines a function used in a transition to describe the progression curve 10 | // from one value to the next. The input 'percent' always ranges from 0.0 to 1.0, where 0.0 11 | // represents the starting point and 1.0 represents the end point of the transition. 12 | // 13 | // The returned value from TransitionFunc is expected to be in the normalized range of [0.0, 1.0]. 14 | // However, it may exceed this range, providing flexibility to generate curves with momentum. 15 | // The Transition() function then maps this normalized output to the actual desired range. 16 | type TransitionFunc func(percent float64) float64 17 | 18 | // TransitionLinear transitions the gain linearly from the start to end value. 19 | func TransitionLinear(percent float64) float64 { 20 | return percent 21 | } 22 | 23 | // TransitionEqualPower transitions the gain of a streamer in such a way that the total perceived volume stays 24 | // constant if mixed together with another streamer doing the inverse transition. 25 | // 26 | // See https://www.oreilly.com/library/view/web-audio-api/9781449332679/ch03.html#s03_2 for more information. 27 | func TransitionEqualPower(percent float64) float64 { 28 | return math.Cos((1.0 - percent) * 0.5 * math.Pi) 29 | } 30 | 31 | // Transition gradually adjusts the gain of the source streamer 's' from 'startGain' to 'endGain' 32 | // over the entire duration of the stream, defined by the number of samples 'len'. 33 | // The transition is defined by the provided 'transitionFunc' function, which determines the 34 | // gain at each point during the transition. 35 | func Transition(s beep.Streamer, len int, startGain, endGain float64, transitionfunc TransitionFunc) *TransitionStreamer { 36 | return &TransitionStreamer{ 37 | s: s, 38 | len: len, 39 | startGain: startGain, 40 | endGain: endGain, 41 | transitionFunc: transitionfunc, 42 | } 43 | } 44 | 45 | type TransitionStreamer struct { 46 | s beep.Streamer 47 | pos int 48 | len int 49 | startGain, endGain float64 50 | transitionFunc TransitionFunc 51 | } 52 | 53 | // Stream fills samples with the gain-adjusted samples of the source streamer. 54 | func (t *TransitionStreamer) Stream(samples [][2]float64) (n int, ok bool) { 55 | n, ok = t.s.Stream(samples) 56 | 57 | for i := 0; i < n; i++ { 58 | pos := t.pos + i 59 | progress := float64(pos) / float64(t.len) 60 | progress = min(progress, 1.0) 61 | value := t.transitionFunc(progress) 62 | gain := t.startGain + (t.endGain-t.startGain)*value 63 | 64 | samples[i][0] *= gain 65 | samples[i][1] *= gain 66 | } 67 | 68 | t.pos += n 69 | 70 | return 71 | } 72 | 73 | // Err propagates the original Streamer's errors. 74 | func (t *TransitionStreamer) Err() error { 75 | return t.s.Err() 76 | } 77 | -------------------------------------------------------------------------------- /effects/transition_test.go: -------------------------------------------------------------------------------- 1 | package effects_test 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gopxl/beep/v2" 7 | "github.com/gopxl/beep/v2/effects" 8 | "github.com/gopxl/beep/v2/generators" 9 | "github.com/gopxl/beep/v2/speaker" 10 | ) 11 | 12 | // Cross-fade between two sine tones. 13 | func ExampleTransition() { 14 | sampleRate := beep.SampleRate(44100) 15 | 16 | s1, err := generators.SineTone(sampleRate, 261.63) 17 | if err != nil { 18 | panic(err) 19 | } 20 | s2, err := generators.SineTone(sampleRate, 329.628) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | crossFades := beep.Seq( 26 | // Play s1 normally for 3 seconds 27 | beep.Take(sampleRate.N(time.Second*3), s1), 28 | // Play s1 and s2 together. s1 transitions from a gain of 1.0 (normal volume) 29 | // to 0.0 (silent) whereas s2 does the opposite. The equal power transition 30 | // function helps keep the overall volume constant. 31 | beep.Mix( 32 | effects.Transition( 33 | beep.Take(sampleRate.N(time.Second*2), s1), 34 | sampleRate.N(time.Second*2), 35 | 1.0, 36 | 0.0, 37 | effects.TransitionEqualPower, 38 | ), 39 | effects.Transition( 40 | beep.Take(sampleRate.N(time.Second*2), s2), 41 | sampleRate.N(time.Second*2), 42 | 0.0, 43 | 1.0, 44 | effects.TransitionEqualPower, 45 | ), 46 | ), 47 | // Play the rest of s2 normally for 3 seconds 48 | beep.Take(sampleRate.N(time.Second*3), s2), 49 | ) 50 | 51 | err = speaker.Init(sampleRate, sampleRate.N(time.Second/30)) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | done := make(chan struct{}) 57 | speaker.Play(beep.Seq( 58 | crossFades, 59 | beep.Callback(func() { 60 | done <- struct{}{} 61 | }), 62 | )) 63 | <-done 64 | } 65 | -------------------------------------------------------------------------------- /effects/volume.go: -------------------------------------------------------------------------------- 1 | package effects 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/gopxl/beep/v2" 7 | ) 8 | 9 | // Volume adjusts the volume of the wrapped Streamer in a human-natural way. Human's perception of 10 | // volume is roughly logarithmic to gain and thus the natural way to adjust volume is exponential. 11 | // 12 | // Natural Base for the exponentiation is somewhere around 2. In order to adjust volume along 13 | // decibells, pick 10 as the Base and set Volume to dB/10. However, adjusting volume along decibells 14 | // is nowhere as natural as with bases around 2. 15 | // 16 | // Volume of 0 means no change. Negative Volume will decrease the perceived volume and positive will 17 | // increase it. 18 | // 19 | // With exponential gain it's impossible to achieve the zero volume. When Silent field is set to 20 | // true, the output is muted. 21 | type Volume struct { 22 | Streamer beep.Streamer 23 | Base float64 24 | Volume float64 25 | Silent bool 26 | } 27 | 28 | // Stream streams the wrapped Streamer with volume adjusted according to Base, Volume and Silent 29 | // fields. 30 | func (v *Volume) Stream(samples [][2]float64) (n int, ok bool) { 31 | n, ok = v.Streamer.Stream(samples) 32 | gain := 0.0 33 | if !v.Silent { 34 | gain = math.Pow(v.Base, v.Volume) 35 | } 36 | for i := range samples[:n] { 37 | samples[i][0] *= gain 38 | samples[i][1] *= gain 39 | } 40 | return n, ok 41 | } 42 | 43 | // Err propagates the wrapped Streamer's errors. 44 | func (v *Volume) Err() error { 45 | return v.Streamer.Err() 46 | } 47 | -------------------------------------------------------------------------------- /examples/doppler-stereo-room/README.md: -------------------------------------------------------------------------------- 1 | # Doppler Stereo Room 2 | 3 | **Use headphones for this one!** 4 | 5 | There are two speakers on the screen: green and blue. The green speaker plays the left stereo channel and the blue speaker plays the right. 6 | 7 | The black square is your head. 8 | 9 | You can move the speakers around as you like. 10 | 11 | The 3D effect is achieved merely by [delaying the sound](https://en.wikipedia.org/wiki/Sound_localization) in one of the ears. Volume is always the same in both ears. 12 | 13 | Things for you to try: 14 | 15 | - Move both speakers to the same side of your head. 16 | - Move one of the speakers past your head at a fast speed. 17 | 18 | ![Screenshot](screenshot.png) 19 | -------------------------------------------------------------------------------- /examples/doppler-stereo-room/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | "time" 8 | "unicode" 9 | 10 | "github.com/gdamore/tcell/v2" 11 | 12 | "github.com/gopxl/beep/v2" 13 | "github.com/gopxl/beep/v2/effects" 14 | "github.com/gopxl/beep/v2/mp3" 15 | "github.com/gopxl/beep/v2/speaker" 16 | ) 17 | 18 | func multiplyChannels(left, right float64, s beep.Streamer) beep.Streamer { 19 | return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 20 | n, ok = s.Stream(samples) 21 | for i := range samples[:n] { 22 | samples[i][0] *= left 23 | samples[i][1] *= right 24 | } 25 | return n, ok 26 | }) 27 | } 28 | 29 | type movingStreamer struct { 30 | x, y float64 31 | velX, velY float64 32 | leftDoppler beep.Streamer 33 | rightDoppler beep.Streamer 34 | } 35 | 36 | func newMovingStreamer(sr beep.SampleRate, x, y float64, streamer beep.Streamer) *movingStreamer { 37 | ms := &movingStreamer{x: x, y: y} 38 | 39 | const metersPerSecond = 343 40 | samplesPerSecond := float64(sr) 41 | samplesPerMeter := samplesPerSecond / metersPerSecond 42 | 43 | leftEar, rightEar := beep.Dup(streamer) 44 | leftEar = multiplyChannels(1, 0, leftEar) 45 | rightEar = multiplyChannels(0, 1, rightEar) 46 | 47 | const earDistance = 0.16 48 | ms.leftDoppler = effects.Doppler(2, samplesPerMeter, leftEar, func(delta int) float64 { 49 | dt := sr.D(delta).Seconds() 50 | ms.x += ms.velX * dt 51 | ms.y += ms.velY * dt 52 | return math.Max(0.25, math.Hypot(ms.x+earDistance/2, ms.y)) 53 | }) 54 | ms.rightDoppler = effects.Doppler(2, samplesPerMeter, rightEar, func(delta int) float64 { 55 | return math.Max(0.25, math.Hypot(ms.x-earDistance/2, ms.y)) 56 | }) 57 | 58 | return ms 59 | } 60 | 61 | func (ms *movingStreamer) play() { 62 | speaker.Play(ms.leftDoppler, ms.rightDoppler) 63 | } 64 | 65 | func drawCircle(screen tcell.Screen, x, y float64, style tcell.Style) { 66 | width, height := screen.Size() 67 | centerX, centerY := float64(width)/2, float64(height)/2 68 | 69 | lx, ly := int(centerX+(x-0.25)*2), int(centerY+y) 70 | screen.SetContent(lx, ly, tcell.RuneBlock, nil, style) 71 | 72 | rx, ry := int(centerX+(x+0.25)*2), int(centerY+y) 73 | screen.SetContent(rx, ry, tcell.RuneBlock, nil, style) 74 | } 75 | 76 | func drawTextLine(screen tcell.Screen, x, y int, s string, style tcell.Style) { 77 | for _, r := range s { 78 | screen.SetContent(x, y, r, nil, style) 79 | x++ 80 | } 81 | } 82 | 83 | func drawHelp(screen tcell.Screen, style tcell.Style) { 84 | drawTextLine(screen, 0, 0, "Welcome to the Doppler Stereo Room!", style) 85 | drawTextLine(screen, 0, 1, "Press [ESC] to quit.", style) 86 | 87 | drawTextLine(screen, 0, 2, "Move the", style) 88 | drawTextLine(screen, 9, 2, "LEFT", style.Background(tcell.ColorGreen).Foreground(tcell.ColorWhiteSmoke)) 89 | drawTextLine(screen, 14, 2, "speaker with WASD.", style) 90 | 91 | drawTextLine(screen, 0, 3, "Move the", style) 92 | drawTextLine(screen, 9, 3, "RIGHT", style.Background(tcell.ColorBlue).Foreground(tcell.ColorWhiteSmoke)) 93 | drawTextLine(screen, 15, 3, "speaker with IJKL.", style) 94 | 95 | drawTextLine(screen, 0, 4, "Move the", style) 96 | drawTextLine(screen, 9, 4, "BOTH", style.Background(tcell.ColorDeepPink).Foreground(tcell.ColorWhiteSmoke)) 97 | drawTextLine(screen, 15, 4, "speakers with the Numpad Buttons 1-9.", style) 98 | 99 | drawTextLine(screen, 0, 5, "Press to start moving, press again to stop. Use [SHIFT] to move fast.", style) 100 | } 101 | 102 | type DirectionMode int 103 | 104 | const ( 105 | _ DirectionMode = iota 106 | Applied 107 | SetPoint 108 | ) 109 | 110 | type EventMappedLocation struct { 111 | lx, 112 | ly, 113 | rx, 114 | ry float64 115 | using DirectionMode 116 | } 117 | 118 | var ResetLocation = EventMappedLocation{-1, 0, 1, 0, SetPoint} 119 | 120 | var directions = map[rune]EventMappedLocation{ 121 | 122 | // Reset 123 | '5': ResetLocation, 124 | 'r': ResetLocation, 125 | 126 | // Numb Pad Layout Mapped 127 | // Front, Back 128 | '8': {-1, -1, 1, -1, SetPoint}, 129 | '2': {-1, 1, 1, 1, SetPoint}, 130 | // Left, Right 131 | '4': {-1.5, 0, -1, 0, SetPoint}, 132 | '6': {1, 0, 1.5, 0, SetPoint}, 133 | // Layout Top Left, Top Right, Bottom Right, Bottom Left 134 | '7': {-1.8, -1.8, -0.8, -1.8, SetPoint}, 135 | '9': {0.8, -1.8, 1.8, -1.8, SetPoint}, 136 | '1': {-1.8, 1.8, -0.8, 1.8, SetPoint}, 137 | '3': {0.8, 1.8, 1.8, 1.8, SetPoint}, 138 | 139 | // Diagonal Locations 140 | '\\': {-1, -1, 1, 1, SetPoint}, 141 | '/': {-1, 1, 1, -1, SetPoint}, 142 | 143 | // Left 144 | 'a': {-1, 0, 0, 0, Applied}, 145 | 'd': {+1, 0, 0, 0, Applied}, 146 | 'w': {0, -1, 0, 0, Applied}, 147 | 's': {0, +1, 0, 0, Applied}, 148 | 149 | // Right 150 | 'j': {0, 0, -1, 0, Applied}, 151 | 'l': {0, 0, +1, 0, Applied}, 152 | 'i': {0, 0, 0, -1, Applied}, 153 | 'k': {0, 0, 0, +1, Applied}, 154 | } 155 | 156 | func main() { 157 | if len(os.Args) != 2 { 158 | fmt.Fprintf(os.Stderr, "Usage: %s song.mp3\n", os.Args[0]) 159 | os.Exit(1) 160 | } 161 | f, err := os.Open(os.Args[1]) 162 | if err != nil { 163 | report(err) 164 | } 165 | streamer, format, err := mp3.Decode(f) 166 | if err != nil { 167 | report(err) 168 | } 169 | defer streamer.Close() 170 | 171 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/30)) 172 | 173 | leftCh, rightCh := beep.Dup(streamer) 174 | 175 | leftCh = effects.Mono(multiplyChannels(1, 0, leftCh)) 176 | rightCh = effects.Mono(multiplyChannels(0, 1, rightCh)) 177 | 178 | leftMS := newMovingStreamer(format.SampleRate, -1, 0, leftCh) 179 | rightMS := newMovingStreamer(format.SampleRate, +1, 0, rightCh) 180 | 181 | leftMS.play() 182 | rightMS.play() 183 | 184 | screen, err := tcell.NewScreen() 185 | if err != nil { 186 | report(err) 187 | } 188 | err = screen.Init() 189 | if err != nil { 190 | report(err) 191 | } 192 | defer screen.Fini() 193 | 194 | frames := time.Tick(time.Second / 30) 195 | events := make(chan tcell.Event) 196 | go func() { 197 | for { 198 | events <- screen.PollEvent() 199 | } 200 | }() 201 | 202 | loop: 203 | for { 204 | select { 205 | case <-frames: 206 | speaker.Lock() 207 | lx, ly := leftMS.x, leftMS.y 208 | rx, ry := rightMS.x, rightMS.y 209 | speaker.Unlock() 210 | 211 | style := tcell.StyleDefault. 212 | Background(tcell.ColorWhiteSmoke). 213 | Foreground(tcell.ColorBlack) 214 | 215 | screen.Clear() 216 | screen.Fill(' ', style) 217 | drawHelp(screen, style) 218 | drawCircle(screen, 0, 0, style.Foreground(tcell.ColorBlack)) 219 | drawCircle(screen, lx*2, ly*2, style.Foreground(tcell.ColorGreen)) 220 | drawCircle(screen, rx*2, ry*2, style.Foreground(tcell.ColorBlue)) 221 | screen.Show() 222 | 223 | case event := <-events: 224 | switch event := event.(type) { 225 | case *tcell.EventKey: 226 | if event.Key() == tcell.KeyESC { 227 | break loop 228 | } 229 | 230 | if event.Key() != tcell.KeyRune { 231 | break 232 | } 233 | 234 | const ( 235 | slowSpeed = 2.0 236 | fastSpeed = 16.0 237 | ) 238 | 239 | speaker.Lock() 240 | 241 | speed := slowSpeed 242 | if unicode.ToLower(event.Rune()) != event.Rune() { 243 | speed = fastSpeed 244 | } 245 | 246 | dir := directions[unicode.ToLower(event.Rune())] 247 | 248 | if dir.using == Applied { 249 | if dir.lx != 0 { 250 | if leftMS.velX == dir.lx*speed { 251 | leftMS.velX = 0 252 | } else { 253 | leftMS.velX = dir.lx * speed 254 | } 255 | } 256 | if dir.ly != 0 { 257 | if leftMS.velY == dir.ly*speed { 258 | leftMS.velY = 0 259 | } else { 260 | leftMS.velY = dir.ly * speed 261 | } 262 | } 263 | if dir.rx != 0 { 264 | if rightMS.velX == dir.rx*speed { 265 | rightMS.velX = 0 266 | } else { 267 | rightMS.velX = dir.rx * speed 268 | } 269 | } 270 | if dir.ry != 0 { 271 | if rightMS.velY == dir.ry*speed { 272 | rightMS.velY = 0 273 | } else { 274 | rightMS.velY = dir.ry * speed 275 | } 276 | } 277 | } else if dir.using == SetPoint { 278 | leftMS.velX = 0 279 | leftMS.velY = 0 280 | rightMS.velX = 0 281 | rightMS.velY = 0 282 | 283 | leftMS.x = dir.lx 284 | leftMS.y = dir.ly 285 | rightMS.x = dir.rx 286 | rightMS.y = dir.ry 287 | } 288 | 289 | speaker.Unlock() 290 | } 291 | } 292 | } 293 | } 294 | 295 | func report(err error) { 296 | fmt.Fprintln(os.Stderr, err) 297 | os.Exit(1) 298 | } 299 | -------------------------------------------------------------------------------- /examples/doppler-stereo-room/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/examples/doppler-stereo-room/screenshot.png -------------------------------------------------------------------------------- /examples/midi/Buy to the Beat - V2 License.md: -------------------------------------------------------------------------------- 1 | # Buy to the Beat - V2 2 | 3 | Copyright (c) 2021 Robert Oost
4 | Soundcloud: https://soundcloud.com/sponsrob 5 | 6 | This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) License. To view a copy of the license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/. 7 | -------------------------------------------------------------------------------- /examples/midi/Buy to the Beat - V2.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/examples/midi/Buy to the Beat - V2.mid -------------------------------------------------------------------------------- /examples/midi/Florestan-Basic-GM-GS License.md: -------------------------------------------------------------------------------- 1 | # Florestan Basic GM GS 2 | 3 | Author: Nando Florestan 4 | 5 | This sound font is in the public domain. 6 | 7 | Source: [Internet Archive](https://archive.org/details/sf2-soundfonts-free-use) 8 | -------------------------------------------------------------------------------- /examples/midi/Florestan-Basic-GM-GS-by-Nando-Florestan(Public-Domain).sf2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/examples/midi/Florestan-Basic-GM-GS-by-Nando-Florestan(Public-Domain).sf2 -------------------------------------------------------------------------------- /examples/midi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/gopxl/beep/v2" 10 | "github.com/gopxl/beep/v2/midi" 11 | "github.com/gopxl/beep/v2/speaker" 12 | ) 13 | 14 | func main() { 15 | var sampleRate beep.SampleRate = 44100 16 | 17 | err := speaker.Init(sampleRate, sampleRate.N(time.Second/30)) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | // Load a soundfont. 23 | soundFontFile, err := os.Open("Florestan-Basic-GM-GS-by-Nando-Florestan(Public-Domain).sf2") 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | soundFont, err := midi.NewSoundFont(soundFontFile) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | // Load a midi track. 33 | midiFile, err := os.Open("Buy to the Beat - V2.mid") 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | s, format, err := midi.Decode(midiFile, soundFont, sampleRate) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | fmt.Printf("Song duration: %v\n", format.SampleRate.D(s.Len())) 43 | speaker.PlayAndWait(s) 44 | } 45 | -------------------------------------------------------------------------------- /examples/speedy-player/README.md: -------------------------------------------------------------------------------- 1 | # Speedy Player 2 | 3 | A simple MP3 player that allows changing the **playback speed**. 4 | 5 | ![Screenshot](screenshot.png) 6 | -------------------------------------------------------------------------------- /examples/speedy-player/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | "unicode" 8 | 9 | "github.com/gdamore/tcell/v2" 10 | 11 | "github.com/gopxl/beep/v2" 12 | "github.com/gopxl/beep/v2/effects" 13 | "github.com/gopxl/beep/v2/mp3" 14 | "github.com/gopxl/beep/v2/speaker" 15 | ) 16 | 17 | func drawTextLine(screen tcell.Screen, x, y int, s string, style tcell.Style) { 18 | for _, r := range s { 19 | screen.SetContent(x, y, r, nil, style) 20 | x++ 21 | } 22 | } 23 | 24 | type audioPanel struct { 25 | sampleRate beep.SampleRate 26 | streamer beep.StreamSeeker 27 | ctrl *beep.Ctrl 28 | resampler *beep.Resampler 29 | volume *effects.Volume 30 | } 31 | 32 | func newAudioPanel(sampleRate beep.SampleRate, streamer beep.StreamSeeker) (*audioPanel, error) { 33 | loopStreamer, err := beep.Loop2(streamer) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | ctrl := &beep.Ctrl{Streamer: loopStreamer} 39 | resampler := beep.ResampleRatio(4, 1, ctrl) 40 | volume := &effects.Volume{Streamer: resampler, Base: 2} 41 | return &audioPanel{sampleRate, streamer, ctrl, resampler, volume}, nil 42 | } 43 | 44 | func (ap *audioPanel) play() { 45 | speaker.Play(ap.volume) 46 | } 47 | 48 | func (ap *audioPanel) draw(screen tcell.Screen) { 49 | mainStyle := tcell.StyleDefault. 50 | Background(tcell.NewHexColor(0x473437)). 51 | Foreground(tcell.NewHexColor(0xD7D8A2)) 52 | statusStyle := mainStyle. 53 | Foreground(tcell.NewHexColor(0xDDC074)). 54 | Bold(true) 55 | 56 | screen.Fill(' ', mainStyle) 57 | 58 | drawTextLine(screen, 0, 0, "Welcome to the Speedy Player!", mainStyle) 59 | drawTextLine(screen, 0, 1, "Press [ESC] to quit.", mainStyle) 60 | drawTextLine(screen, 0, 2, "Press [SPACE] to pause/resume.", mainStyle) 61 | drawTextLine(screen, 0, 3, "Use keys in (?/?) to turn the buttons.", mainStyle) 62 | 63 | speaker.Lock() 64 | position := ap.sampleRate.D(ap.streamer.Position()) 65 | length := ap.sampleRate.D(ap.streamer.Len()) 66 | volume := ap.volume.Volume 67 | speed := ap.resampler.Ratio() 68 | speaker.Unlock() 69 | 70 | positionStatus := fmt.Sprintf("%v / %v", position.Round(time.Second), length.Round(time.Second)) 71 | volumeStatus := fmt.Sprintf("%.1f", volume) 72 | speedStatus := fmt.Sprintf("%.3fx", speed) 73 | 74 | drawTextLine(screen, 0, 5, "Position (Q/W):", mainStyle) 75 | drawTextLine(screen, 16, 5, positionStatus, statusStyle) 76 | 77 | drawTextLine(screen, 0, 6, "Volume (A/S):", mainStyle) 78 | drawTextLine(screen, 16, 6, volumeStatus, statusStyle) 79 | 80 | drawTextLine(screen, 0, 7, "Speed (Z/X):", mainStyle) 81 | drawTextLine(screen, 16, 7, speedStatus, statusStyle) 82 | } 83 | 84 | func (ap *audioPanel) handle(event tcell.Event) (changed, quit bool) { 85 | switch event := event.(type) { 86 | case *tcell.EventKey: 87 | if event.Key() == tcell.KeyESC { 88 | return false, true 89 | } 90 | 91 | if event.Key() != tcell.KeyRune { 92 | return false, false 93 | } 94 | 95 | switch unicode.ToLower(event.Rune()) { 96 | case ' ': 97 | speaker.Lock() 98 | ap.ctrl.Paused = !ap.ctrl.Paused 99 | speaker.Unlock() 100 | return false, false 101 | 102 | case 'q', 'w': 103 | speaker.Lock() 104 | newPos := ap.streamer.Position() 105 | if event.Rune() == 'q' { 106 | newPos -= ap.sampleRate.N(time.Second) 107 | } 108 | if event.Rune() == 'w' { 109 | newPos += ap.sampleRate.N(time.Second) 110 | } 111 | // Clamp the position to be within the stream 112 | newPos = max(newPos, 0) 113 | newPos = min(newPos, ap.streamer.Len()-1) 114 | 115 | if err := ap.streamer.Seek(newPos); err != nil { 116 | report(err) 117 | } 118 | speaker.Unlock() 119 | return true, false 120 | 121 | case 'a': 122 | speaker.Lock() 123 | ap.volume.Volume -= 0.1 124 | speaker.Unlock() 125 | return true, false 126 | 127 | case 's': 128 | speaker.Lock() 129 | ap.volume.Volume += 0.1 130 | speaker.Unlock() 131 | return true, false 132 | 133 | case 'z': 134 | speaker.Lock() 135 | newRatio := ap.resampler.Ratio() * 15 / 16 136 | newRatio = max(newRatio, 0.001) // Limit to a reasonable ratio 137 | ap.resampler.SetRatio(newRatio) 138 | speaker.Unlock() 139 | return true, false 140 | 141 | case 'x': 142 | speaker.Lock() 143 | newRatio := ap.resampler.Ratio() * 16 / 15 144 | newRatio = min(newRatio, 100) // Limit to a reasonable ratio 145 | ap.resampler.SetRatio(newRatio) 146 | speaker.Unlock() 147 | return true, false 148 | } 149 | } 150 | return false, false 151 | } 152 | 153 | func main() { 154 | if len(os.Args) != 2 { 155 | fmt.Fprintf(os.Stderr, "Usage: %s song.mp3\n", os.Args[0]) 156 | os.Exit(1) 157 | } 158 | f, err := os.Open(os.Args[1]) 159 | if err != nil { 160 | report(err) 161 | } 162 | streamer, format, err := mp3.Decode(f) 163 | if err != nil { 164 | report(err) 165 | } 166 | defer streamer.Close() 167 | 168 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/30)) 169 | 170 | screen, err := tcell.NewScreen() 171 | if err != nil { 172 | report(err) 173 | } 174 | err = screen.Init() 175 | if err != nil { 176 | report(err) 177 | } 178 | defer screen.Fini() 179 | 180 | ap, err := newAudioPanel(format.SampleRate, streamer) 181 | if err != nil { 182 | report(err) 183 | } 184 | 185 | screen.Clear() 186 | ap.draw(screen) 187 | screen.Show() 188 | 189 | ap.play() 190 | 191 | seconds := time.Tick(time.Second) 192 | events := make(chan tcell.Event) 193 | go func() { 194 | for { 195 | events <- screen.PollEvent() 196 | } 197 | }() 198 | 199 | loop: 200 | for { 201 | select { 202 | case event := <-events: 203 | changed, quit := ap.handle(event) 204 | if quit { 205 | break loop 206 | } 207 | if changed { 208 | screen.Clear() 209 | ap.draw(screen) 210 | screen.Show() 211 | } 212 | case <-seconds: 213 | screen.Clear() 214 | ap.draw(screen) 215 | screen.Show() 216 | } 217 | } 218 | } 219 | 220 | func report(err error) { 221 | fmt.Fprintln(os.Stderr, err) 222 | os.Exit(1) 223 | } 224 | -------------------------------------------------------------------------------- /examples/speedy-player/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/examples/speedy-player/screenshot.png -------------------------------------------------------------------------------- /examples/tone-player/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/gopxl/beep/v2" 10 | "github.com/gopxl/beep/v2/generators" 11 | "github.com/gopxl/beep/v2/speaker" 12 | ) 13 | 14 | func usage() { 15 | fmt.Printf("usage: %s freq\n", os.Args[0]) 16 | fmt.Println("where freq must be a float between 1 and 24000") 17 | fmt.Println("24000 because samplerate of 48000 is hardcoded") 18 | } 19 | 20 | func main() { 21 | if len(os.Args) < 2 { 22 | usage() 23 | return 24 | } 25 | 26 | f, err := strconv.ParseFloat(os.Args[1], 64) 27 | if err != nil { 28 | usage() 29 | return 30 | } 31 | 32 | sr := beep.SampleRate(48000) 33 | speaker.Init(sr, 4800) 34 | 35 | sine, err := generators.SineTone(sr, f) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | triangle, err := generators.TriangleTone(sr, f) 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | square, err := generators.SquareTone(sr, f) 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | sawtooth, err := generators.SawtoothTone(sr, f) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | sawtoothReversed, err := generators.SawtoothToneReversed(sr, f) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | // Play 2 seconds of each tone 61 | two := sr.N(2 * time.Second) 62 | 63 | ch := make(chan struct{}) 64 | sounds := []beep.Streamer{ 65 | beep.Callback(print("sine")), 66 | beep.Take(two, sine), 67 | beep.Callback(print("triangle")), 68 | beep.Take(two, triangle), 69 | beep.Callback(print("square")), 70 | beep.Take(two, square), 71 | beep.Callback(print("sawtooth")), 72 | beep.Take(two, sawtooth), 73 | beep.Callback(print("sawtooth reversed")), 74 | beep.Take(two, sawtoothReversed), 75 | beep.Callback(func() { 76 | ch <- struct{}{} 77 | }), 78 | } 79 | speaker.Play(beep.Seq(sounds...)) 80 | 81 | <-ch 82 | } 83 | 84 | // a simple clousure to wrap fmt.Println 85 | func print(s string) func() { 86 | return func() { 87 | fmt.Println(s) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/tutorial/1-hello-beep/Lame_Drivers_-_01_-_Frozen_Egg.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/examples/tutorial/1-hello-beep/Lame_Drivers_-_01_-_Frozen_Egg.mp3 -------------------------------------------------------------------------------- /examples/tutorial/1-hello-beep/a/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | "github.com/gopxl/beep/v2" 9 | "github.com/gopxl/beep/v2/mp3" 10 | "github.com/gopxl/beep/v2/speaker" 11 | ) 12 | 13 | func main() { 14 | f, err := os.Open("../Lame_Drivers_-_01_-_Frozen_Egg.mp3") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | 19 | streamer, format, err := mp3.Decode(f) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | defer streamer.Close() 24 | 25 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) 26 | 27 | done := make(chan bool) 28 | speaker.Play(beep.Seq(streamer, beep.Callback(func() { 29 | done <- true 30 | }))) 31 | 32 | <-done 33 | } 34 | -------------------------------------------------------------------------------- /examples/tutorial/1-hello-beep/b/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | "github.com/gopxl/beep/v2" 9 | "github.com/gopxl/beep/v2/mp3" 10 | "github.com/gopxl/beep/v2/speaker" 11 | ) 12 | 13 | func main() { 14 | f, err := os.Open("../Lame_Drivers_-_01_-_Frozen_Egg.mp3") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | 19 | streamer, format, err := mp3.Decode(f) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | defer streamer.Close() 24 | 25 | sr := format.SampleRate * 2 26 | speaker.Init(sr, sr.N(time.Second/10)) 27 | 28 | resampled := beep.Resample(4, format.SampleRate, sr, streamer) 29 | 30 | done := make(chan bool) 31 | speaker.Play(beep.Seq(resampled, beep.Callback(func() { 32 | done <- true 33 | }))) 34 | 35 | <-done 36 | } 37 | -------------------------------------------------------------------------------- /examples/tutorial/2-composing-and-controlling/Miami_Slice_-_04_-_Step_Into_Me.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/examples/tutorial/2-composing-and-controlling/Miami_Slice_-_04_-_Step_Into_Me.mp3 -------------------------------------------------------------------------------- /examples/tutorial/2-composing-and-controlling/a/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/gopxl/beep/v2" 10 | "github.com/gopxl/beep/v2/mp3" 11 | "github.com/gopxl/beep/v2/speaker" 12 | ) 13 | 14 | func main() { 15 | f, err := os.Open("../Miami_Slice_-_04_-_Step_Into_Me.mp3") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | streamer, format, err := mp3.Decode(f) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | defer streamer.Close() 25 | 26 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) 27 | 28 | done := make(chan bool) 29 | speaker.Play(beep.Seq(streamer, beep.Callback(func() { 30 | done <- true 31 | }))) 32 | 33 | for { 34 | select { 35 | case <-done: 36 | return 37 | case <-time.After(time.Second): 38 | speaker.Lock() 39 | fmt.Println(format.SampleRate.D(streamer.Position()).Round(time.Second)) 40 | speaker.Unlock() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/tutorial/2-composing-and-controlling/b/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/gopxl/beep/v2" 10 | "github.com/gopxl/beep/v2/mp3" 11 | "github.com/gopxl/beep/v2/speaker" 12 | ) 13 | 14 | func main() { 15 | f, err := os.Open("../Miami_Slice_-_04_-_Step_Into_Me.mp3") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | streamer, format, err := mp3.Decode(f) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | defer streamer.Close() 25 | 26 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) 27 | 28 | loopStreamer, err := beep.Loop2(streamer, beep.LoopTimes(2)) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | fast := beep.ResampleRatio(4, 5, loopStreamer) 33 | 34 | done := make(chan bool) 35 | speaker.Play(beep.Seq(fast, beep.Callback(func() { 36 | done <- true 37 | }))) 38 | 39 | for { 40 | select { 41 | case <-done: 42 | return 43 | case <-time.After(time.Second): 44 | speaker.Lock() 45 | fmt.Println(format.SampleRate.D(streamer.Position()).Round(time.Second)) 46 | speaker.Unlock() 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/tutorial/2-composing-and-controlling/c/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/gopxl/beep/v2" 10 | "github.com/gopxl/beep/v2/mp3" 11 | "github.com/gopxl/beep/v2/speaker" 12 | ) 13 | 14 | func main() { 15 | f, err := os.Open("../Miami_Slice_-_04_-_Step_Into_Me.mp3") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | streamer, format, err := mp3.Decode(f) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | defer streamer.Close() 25 | 26 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) 27 | 28 | loopStreamer, err := beep.Loop2(streamer) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | ctrl := &beep.Ctrl{Streamer: loopStreamer, Paused: false} 34 | speaker.Play(ctrl) 35 | 36 | for { 37 | fmt.Print("Press [ENTER] to pause/resume. ") 38 | fmt.Scanln() 39 | 40 | speaker.Lock() 41 | ctrl.Paused = !ctrl.Paused 42 | speaker.Unlock() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/tutorial/2-composing-and-controlling/d/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/gopxl/beep/v2" 10 | "github.com/gopxl/beep/v2/effects" 11 | "github.com/gopxl/beep/v2/mp3" 12 | "github.com/gopxl/beep/v2/speaker" 13 | ) 14 | 15 | func main() { 16 | f, err := os.Open("../Miami_Slice_-_04_-_Step_Into_Me.mp3") 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | streamer, format, err := mp3.Decode(f) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | defer streamer.Close() 26 | 27 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) 28 | loopStreamer, err := beep.Loop2(streamer) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | ctrl := &beep.Ctrl{Streamer: loopStreamer, Paused: false} 34 | volume := &effects.Volume{ 35 | Streamer: ctrl, 36 | Base: 2, 37 | Volume: 0, 38 | Silent: false, 39 | } 40 | speedy := beep.ResampleRatio(4, 1, volume) 41 | speaker.Play(speedy) 42 | 43 | for { 44 | fmt.Print("Press [ENTER] to pause/resume. ") 45 | fmt.Scanln() 46 | 47 | speaker.Lock() 48 | ctrl.Paused = !ctrl.Paused 49 | volume.Volume += 0.5 50 | speedy.SetRatio(speedy.Ratio() + 0.1) 51 | speaker.Unlock() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/tutorial/3-to-buffer-or-not-to-buffer/gunshot.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/examples/tutorial/3-to-buffer-or-not-to-buffer/gunshot.mp3 -------------------------------------------------------------------------------- /examples/tutorial/3-to-buffer-or-not-to-buffer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/gopxl/beep/v2" 10 | "github.com/gopxl/beep/v2/mp3" 11 | "github.com/gopxl/beep/v2/speaker" 12 | ) 13 | 14 | func main() { 15 | f, err := os.Open("gunshot.mp3") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | streamer, format, err := mp3.Decode(f) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/60)) 26 | 27 | buffer := beep.NewBuffer(format) 28 | buffer.Append(streamer) 29 | streamer.Close() 30 | 31 | for { 32 | fmt.Print("Press [ENTER] to fire a gunshot! ") 33 | fmt.Scanln() 34 | 35 | shot := buffer.Streamer(0, buffer.Len()) 36 | speaker.Play(shot) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/tutorial/4-making-own-streamers/a/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/gopxl/beep/v2" 8 | "github.com/gopxl/beep/v2/speaker" 9 | ) 10 | 11 | func Noise() beep.Streamer { 12 | return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 13 | for i := range samples { 14 | samples[i][0] = rand.Float64()*2 - 1 15 | samples[i][1] = rand.Float64()*2 - 1 16 | } 17 | return len(samples), true 18 | }) 19 | } 20 | 21 | func main() { 22 | sr := beep.SampleRate(44100) 23 | speaker.Init(sr, sr.N(time.Second/10)) 24 | speaker.Play(Noise()) 25 | select {} 26 | } 27 | -------------------------------------------------------------------------------- /examples/tutorial/4-making-own-streamers/b/frozen-egg.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/examples/tutorial/4-making-own-streamers/b/frozen-egg.mp3 -------------------------------------------------------------------------------- /examples/tutorial/4-making-own-streamers/b/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/gopxl/beep/v2" 9 | "github.com/gopxl/beep/v2/mp3" 10 | "github.com/gopxl/beep/v2/speaker" 11 | ) 12 | 13 | type Queue struct { 14 | streamers []beep.Streamer 15 | } 16 | 17 | func (q *Queue) Add(streamers ...beep.Streamer) { 18 | q.streamers = append(q.streamers, streamers...) 19 | } 20 | 21 | func (q *Queue) Stream(samples [][2]float64) (n int, ok bool) { 22 | // We use the filled variable to track how many samples we've 23 | // successfully filled already. We loop until all samples are filled. 24 | filled := 0 25 | for filled < len(samples) { 26 | // There are no streamers in the queue, so we stream silence. 27 | if len(q.streamers) == 0 { 28 | for i := range samples[filled:] { 29 | samples[i][0] = 0 30 | samples[i][1] = 0 31 | } 32 | break 33 | } 34 | 35 | // We stream from the first streamer in the queue. 36 | n, ok := q.streamers[0].Stream(samples[filled:]) 37 | // If it's drained, we pop it from the queue, thus continuing with 38 | // the next streamer. 39 | if !ok { 40 | q.streamers = q.streamers[1:] 41 | } 42 | // We update the number of filled samples. 43 | filled += n 44 | } 45 | return len(samples), true 46 | } 47 | 48 | func (q *Queue) Err() error { 49 | return nil 50 | } 51 | 52 | func main() { 53 | sr := beep.SampleRate(44100) 54 | speaker.Init(sr, sr.N(time.Second/10)) 55 | 56 | // A zero Queue is an empty Queue. 57 | var queue Queue 58 | speaker.Play(&queue) 59 | 60 | for { 61 | var name string 62 | fmt.Print("Type an MP3 file name: ") 63 | fmt.Scanln(&name) 64 | 65 | // Open the file on the disk. 66 | f, err := os.Open(name) 67 | if err != nil { 68 | fmt.Println(err) 69 | continue 70 | } 71 | 72 | // Decode it. 73 | streamer, format, err := mp3.Decode(f) 74 | if err != nil { 75 | fmt.Println(err) 76 | continue 77 | } 78 | 79 | // The speaker's sample rate is fixed at 44100. Therefore, we need to 80 | // resample the file in case it's in a different sample rate. 81 | resampled := beep.Resample(4, format.SampleRate, sr, streamer) 82 | 83 | // And finally, we add the song to the queue. 84 | speaker.Lock() 85 | queue.Add(resampled) 86 | speaker.Unlock() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /examples/tutorial/4-making-own-streamers/b/step-into-me.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/examples/tutorial/4-making-own-streamers/b/step-into-me.mp3 -------------------------------------------------------------------------------- /examples/tutorial/5-equalizer/mono/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/gopxl/beep/v2" 8 | "github.com/gopxl/beep/v2/effects" 9 | "github.com/gopxl/beep/v2/speaker" 10 | ) 11 | 12 | func noise() beep.Streamer { 13 | return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 14 | for i := range samples { 15 | samples[i][0] = rand.Float64()*2 - 1 16 | samples[i][1] = rand.Float64()*2 - 1 17 | } 18 | return len(samples), true 19 | }) 20 | } 21 | 22 | func main() { 23 | sr := beep.SampleRate(44100) 24 | speaker.Init(sr, sr.N(time.Second/10)) 25 | 26 | eq := effects.NewEqualizer(noise(), sr, effects.MonoEqualizerSections{ 27 | {F0: 200, Bf: 5, GB: 3, G0: 0, G: 8}, 28 | {F0: 250, Bf: 5, GB: 3, G0: 0, G: 10}, 29 | {F0: 300, Bf: 5, GB: 3, G0: 0, G: 12}, 30 | {F0: 350, Bf: 5, GB: 3, G0: 0, G: 14}, 31 | {F0: 10000, Bf: 8000, GB: 3, G0: 0, G: -100}, 32 | }) 33 | 34 | speaker.Play(eq) 35 | select {} 36 | } 37 | -------------------------------------------------------------------------------- /examples/tutorial/5-equalizer/stereo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/gopxl/beep/v2" 8 | "github.com/gopxl/beep/v2/effects" 9 | "github.com/gopxl/beep/v2/speaker" 10 | ) 11 | 12 | func noise() beep.Streamer { 13 | return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 14 | for i := range samples { 15 | samples[i][0] = rand.Float64()*2 - 1 16 | samples[i][1] = rand.Float64()*2 - 1 17 | } 18 | return len(samples), true 19 | }) 20 | } 21 | 22 | func main() { 23 | sr := beep.SampleRate(44100) 24 | speaker.Init(sr, sr.N(time.Second/10)) 25 | 26 | eq := effects.NewEqualizer(noise(), sr, effects.StereoEqualizerSections{ 27 | { 28 | Left: effects.MonoEqualizerSection{F0: 200, Bf: 5, GB: 3, G0: 0, G: 8}, 29 | Right: effects.MonoEqualizerSection{F0: 200, Bf: 5, GB: 3, G0: 0, G: -8}, 30 | }, 31 | { 32 | Left: effects.MonoEqualizerSection{F0: 10000, Bf: 1000, GB: 3, G0: 0, G: 90}, 33 | Right: effects.MonoEqualizerSection{F0: 10000, Bf: 1000, GB: 3, G0: 0, G: -90}, 34 | }, 35 | }) 36 | 37 | speaker.Play(eq) 38 | select {} 39 | } 40 | -------------------------------------------------------------------------------- /examples/tutorial/README.md: -------------------------------------------------------------------------------- 1 | # Code for the [tutorials](https://github.com/gopxl/beep/wiki) 2 | 3 | Here's the code for all the tutorials from the [Wiki](https://github.com/gopxl/beep/wiki/Hello,-Beep!). 4 | 5 | All the music is downloaded from [Free Music Archive](https://freemusicarchive.org) and [Free Sound Effects.com](https://www.freesoundeffects.com/). 6 | -------------------------------------------------------------------------------- /flac/decode.go: -------------------------------------------------------------------------------- 1 | package flac 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/mewkiz/flac" 7 | "github.com/mewkiz/flac/frame" 8 | "github.com/pkg/errors" 9 | 10 | "github.com/gopxl/beep/v2" 11 | ) 12 | 13 | // Decode takes a Reader containing audio data in FLAC format and returns a StreamSeekCloser, 14 | // which streams that audio. The Seek method will panic if r is not io.Seeker. 15 | // 16 | // Do not close the supplied Reader, instead, use the Close method of the returned 17 | // StreamSeekCloser when you want to release the resources. 18 | func Decode(r io.Reader) (s beep.StreamSeekCloser, format beep.Format, err error) { 19 | d := decoder{r: r} 20 | defer func() { // hacky way to always close r if an error occurred 21 | if closer, ok := d.r.(io.Closer); ok { 22 | if err != nil { 23 | closer.Close() 24 | } 25 | } 26 | }() 27 | 28 | rs, seeker := r.(io.ReadSeeker) 29 | if seeker { 30 | d.stream, err = flac.NewSeek(rs) 31 | d.seekEnabled = true 32 | } else { 33 | d.stream, err = flac.New(r) 34 | } 35 | if err != nil { 36 | return nil, beep.Format{}, errors.Wrap(err, "flac") 37 | } 38 | 39 | // Read the first frame 40 | d.frame, err = d.stream.ParseNext() 41 | if err != nil { 42 | return nil, beep.Format{}, errors.Wrap(err, "flac") 43 | } 44 | d.hasFixedBlockSize = d.frame.HasFixedBlockSize 45 | 46 | format = beep.Format{ 47 | SampleRate: beep.SampleRate(d.stream.Info.SampleRate), 48 | NumChannels: int(d.stream.Info.NChannels), 49 | Precision: int(d.stream.Info.BitsPerSample / 8), 50 | } 51 | return &d, format, nil 52 | } 53 | 54 | type decoder struct { 55 | r io.Reader 56 | stream *flac.Stream 57 | frame *frame.Frame 58 | posInFrame int 59 | err error 60 | seekEnabled bool 61 | 62 | hasFixedBlockSize bool 63 | } 64 | 65 | func (d *decoder) Stream(samples [][2]float64) (n int, ok bool) { 66 | if d.err != nil || d.frame == nil { 67 | return 0, false 68 | } 69 | 70 | for len(samples) > 0 { 71 | samplesLeft := int(d.frame.BlockSize) - d.posInFrame 72 | if samplesLeft <= 0 { 73 | // Read next frame 74 | var err error 75 | d.frame, err = d.stream.ParseNext() 76 | if err != nil { 77 | d.frame = nil 78 | if err == io.EOF { 79 | return n, n > 0 80 | } 81 | d.err = errors.Wrap(err, "flac") 82 | return 0, false 83 | } 84 | d.posInFrame = 0 85 | continue 86 | } 87 | 88 | toFill := min(samplesLeft, len(samples)) 89 | d.decodeFrameRangeInto(d.frame, d.posInFrame, toFill, samples) 90 | d.posInFrame += toFill 91 | n += toFill 92 | samples = samples[toFill:] 93 | } 94 | 95 | return n, true 96 | } 97 | 98 | // decodeFrameRangeInto decodes the samples frame from the position `start` up to `start + num` 99 | // and stores them in Beep's format into the provided slice `into`. 100 | func (d *decoder) decodeFrameRangeInto(frame *frame.Frame, start, num int, into [][2]float64) { 101 | bps := d.stream.Info.BitsPerSample 102 | numChannels := d.stream.Info.NChannels 103 | s := 1 << (bps - 1) 104 | q := 1 / float64(s) 105 | 106 | if numChannels == 1 { 107 | samples1 := frame.Subframes[0].Samples[start:] 108 | for i := 0; i < num; i++ { 109 | v := float64(samples1[i]) * q 110 | into[i][0] = v 111 | into[i][1] = v 112 | } 113 | } else { 114 | samples1 := frame.Subframes[0].Samples[start:] 115 | samples2 := frame.Subframes[1].Samples[start:] 116 | for i := 0; i < num; i++ { 117 | into[i][0] = float64(samples1[i]) * q 118 | into[i][1] = float64(samples2[i]) * q 119 | } 120 | } 121 | } 122 | 123 | func (d *decoder) Err() error { 124 | return d.err 125 | } 126 | 127 | func (d *decoder) Len() int { 128 | return int(d.stream.Info.NSamples) 129 | } 130 | 131 | func (d *decoder) Position() int { 132 | if d.frame == nil { 133 | return d.Len() 134 | } 135 | 136 | // Temporary workaround until https://github.com/mewkiz/flac/pull/73 is resolved. 137 | if d.hasFixedBlockSize { 138 | return int(d.frame.Num)*int(d.stream.Info.BlockSizeMax) + d.posInFrame 139 | } 140 | 141 | return int(d.frame.SampleNumber()) + d.posInFrame 142 | } 143 | 144 | func (d *decoder) Seek(p int) error { 145 | if !d.seekEnabled { 146 | return errors.New("flac.decoder.Seek: not enabled") 147 | } 148 | 149 | // Temporary workaround until https://github.com/mewkiz/flac/pull/73 is resolved. 150 | // frame.SampleNumber() doesn't work for the last frame of a fixed block size stream 151 | // with the result that seeking to that frame doesn't work either. Therefore, if such 152 | // a seek is requested, we seek to one of the frames before it and consume until the 153 | // desired position is reached. 154 | if d.hasFixedBlockSize { 155 | lastFrameStartLowerBound := d.Len() - int(d.stream.Info.BlockSizeMax) 156 | if p >= lastFrameStartLowerBound { 157 | // Seek to & consume an earlier frame. 158 | _, err := d.stream.Seek(uint64(lastFrameStartLowerBound - 1)) 159 | if err != nil { 160 | return errors.Wrap(err, "flac") 161 | } 162 | for { 163 | d.frame, err = d.stream.ParseNext() 164 | if err != nil { 165 | return errors.Wrap(err, "flac") 166 | } 167 | // Calculate the frame start position manually, because this doesn't 168 | // work for the last frame. 169 | frameStart := d.frame.Num * uint64(d.stream.Info.BlockSizeMax) 170 | if frameStart+uint64(d.frame.BlockSize) >= d.stream.Info.NSamples { 171 | // Found the desired frame. 172 | d.posInFrame = p - int(frameStart) 173 | return nil 174 | } 175 | } 176 | } 177 | } 178 | 179 | // d.stream.Seek() doesn't seek to the exact position p, instead 180 | // it seeks to the start of the frame p is in. The frame position 181 | // is returned and stored in pos. 182 | pos, err := d.stream.Seek(uint64(p)) 183 | if err != nil { 184 | return errors.Wrap(err, "flac") 185 | } 186 | d.posInFrame = p - int(pos) 187 | 188 | d.frame, err = d.stream.ParseNext() 189 | if err != nil { 190 | return errors.Wrap(err, "flac") 191 | } 192 | 193 | return nil 194 | } 195 | 196 | func (d *decoder) Close() error { 197 | if closer, ok := d.r.(io.Closer); ok { 198 | err := closer.Close() 199 | if err != nil { 200 | return errors.Wrap(err, "flac") 201 | } 202 | } 203 | return nil 204 | } 205 | -------------------------------------------------------------------------------- /flac/decode_test.go: -------------------------------------------------------------------------------- 1 | package flac_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "log" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/gopxl/beep/v2/flac" 13 | "github.com/gopxl/beep/v2/internal/testtools" 14 | "github.com/gopxl/beep/v2/wav" 15 | 16 | mewkiz_flac "github.com/mewkiz/flac" 17 | ) 18 | 19 | func TestDecoder_ReturnBehaviour(t *testing.T) { 20 | f, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples_ffmpeg.flac")) 21 | assert.NoError(t, err) 22 | defer f.Close() 23 | 24 | s, _, err := flac.Decode(f) 25 | assert.NoError(t, err) 26 | assert.Equal(t, 22050, s.Len()) 27 | 28 | testtools.AssertStreamerHasCorrectReturnBehaviour(t, s, s.Len()) 29 | } 30 | 31 | func TestDecoder_Stream(t *testing.T) { 32 | flacFile, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples_ffmpeg.flac")) 33 | assert.NoError(t, err) 34 | defer flacFile.Close() 35 | 36 | // Use WAV file as reference. Since both FLAC and WAV are lossless, comparing 37 | // the samples should be possible (allowing for some floating point errors). 38 | wavFile, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples.wav")) 39 | assert.NoError(t, err) 40 | defer wavFile.Close() 41 | 42 | flacStream, _, err := flac.Decode(flacFile) 43 | assert.NoError(t, err) 44 | 45 | wavStream, _, err := wav.Decode(wavFile) 46 | assert.NoError(t, err) 47 | 48 | assert.Equal(t, 22050, wavStream.Len()) 49 | assert.Equal(t, 22050, flacStream.Len()) 50 | 51 | wavSamples := testtools.Collect(wavStream) 52 | flacSamples := testtools.Collect(flacStream) 53 | 54 | testtools.AssertSamplesEqual(t, wavSamples, flacSamples) 55 | } 56 | 57 | func TestDecoder_Seek(t *testing.T) { 58 | flacFile, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples_ffmpeg.flac")) 59 | assert.NoError(t, err) 60 | defer flacFile.Close() 61 | 62 | // Use WAV file as reference. Since both FLAC and WAV are lossless, comparing 63 | // the samples should be possible (allowing for some floating point errors). 64 | wavFile, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples.wav")) 65 | assert.NoError(t, err) 66 | defer wavFile.Close() 67 | 68 | // Get the frame numbers from the FLAC files manually, so that we 69 | // can explicitly test difficult Seek positions. 70 | frameStarts, err := getFlacFrameStartPositions(flacFile) 71 | assert.NoError(t, err) 72 | _, err = flacFile.Seek(0, io.SeekStart) 73 | assert.NoError(t, err) 74 | 75 | flacStream, _, err := flac.Decode(flacFile) 76 | assert.NoError(t, err) 77 | 78 | wavStream, _, err := wav.Decode(wavFile) 79 | assert.NoError(t, err) 80 | 81 | assert.Equal(t, wavStream.Len(), flacStream.Len()) 82 | 83 | // Test start of 2nd frame 84 | seekPos := int(frameStarts[1]) 85 | err = wavStream.Seek(seekPos) 86 | assert.NoError(t, err) 87 | assert.Equal(t, seekPos, wavStream.Position()) 88 | err = flacStream.Seek(seekPos) 89 | assert.NoError(t, err) 90 | assert.Equal(t, seekPos, flacStream.Position()) 91 | 92 | wavSamples := testtools.CollectNum(100, wavStream) 93 | flacSamples := testtools.CollectNum(100, flacStream) 94 | testtools.AssertSamplesEqual(t, wavSamples, flacSamples) 95 | 96 | // Test middle of 2nd frame 97 | seekPos = (int(frameStarts[1]) + int(frameStarts[2])) / 2 98 | err = wavStream.Seek(seekPos) 99 | assert.NoError(t, err) 100 | assert.Equal(t, seekPos, wavStream.Position()) 101 | err = flacStream.Seek(seekPos) 102 | assert.NoError(t, err) 103 | assert.Equal(t, seekPos, flacStream.Position()) 104 | 105 | wavSamples = testtools.CollectNum(100, wavStream) 106 | flacSamples = testtools.CollectNum(100, flacStream) 107 | testtools.AssertSamplesEqual(t, wavSamples, flacSamples) 108 | 109 | // Test end of 2nd frame 110 | seekPos = int(frameStarts[2]) - 1 111 | err = wavStream.Seek(seekPos) 112 | assert.NoError(t, err) 113 | assert.Equal(t, seekPos, wavStream.Position()) 114 | err = flacStream.Seek(seekPos) 115 | assert.NoError(t, err) 116 | assert.Equal(t, seekPos, flacStream.Position()) 117 | 118 | wavSamples = testtools.CollectNum(100, wavStream) 119 | flacSamples = testtools.CollectNum(100, flacStream) 120 | testtools.AssertSamplesEqual(t, wavSamples, flacSamples) 121 | 122 | // Test end of stream. 123 | seekPos = wavStream.Len() - 1 124 | err = wavStream.Seek(seekPos) 125 | assert.NoError(t, err) 126 | assert.Equal(t, seekPos, wavStream.Position()) 127 | err = flacStream.Seek(seekPos) 128 | assert.NoError(t, err) 129 | assert.Equal(t, seekPos, flacStream.Position()) 130 | 131 | wavSamples = testtools.CollectNum(100, wavStream) 132 | flacSamples = testtools.CollectNum(100, flacStream) 133 | testtools.AssertSamplesEqual(t, wavSamples, flacSamples) 134 | 135 | // Test after end of stream. 136 | seekPos = wavStream.Len() 137 | err = wavStream.Seek(seekPos) 138 | assert.NoError(t, err) 139 | assert.Equal(t, seekPos, wavStream.Position()) 140 | err = flacStream.Seek(seekPos) 141 | assert.NoError(t, err) 142 | assert.Equal(t, seekPos, flacStream.Position()) 143 | 144 | wavSamples = testtools.CollectNum(100, wavStream) 145 | flacSamples = testtools.CollectNum(100, flacStream) 146 | testtools.AssertSamplesEqual(t, wavSamples, flacSamples) 147 | } 148 | 149 | func getFlacFrameStartPositions(r io.Reader) ([]uint64, error) { 150 | stream, err := mewkiz_flac.New(r) 151 | if err != nil { 152 | log.Fatal(err) 153 | } 154 | defer stream.Close() 155 | 156 | var frameStarts []uint64 157 | for { 158 | frame, err := stream.ParseNext() 159 | if err != nil { 160 | if err == io.EOF { 161 | break 162 | } 163 | return nil, err 164 | } 165 | frameStarts = append(frameStarts, frame.SampleNumber()) 166 | } 167 | 168 | return frameStarts, nil 169 | } 170 | 171 | func BenchmarkDecoder_Stream(b *testing.B) { 172 | // Load the file into memory, so the disk performance doesn't impact the benchmark. 173 | data, err := os.ReadFile(testtools.TestFilePath("valid_44100hz_22050_samples_ffmpeg.flac")) 174 | assert.NoError(b, err) 175 | 176 | r := bytes.NewReader(data) 177 | 178 | b.Run("test", func(b *testing.B) { 179 | s, _, err := flac.Decode(r) 180 | assert.NoError(b, err) 181 | 182 | samples := testtools.Collect(s) 183 | assert.Equal(b, 22050, len(samples)) 184 | 185 | // Reset for next run. 186 | _, err = r.Seek(0, io.SeekStart) 187 | assert.NoError(b, err) 188 | }) 189 | } 190 | -------------------------------------------------------------------------------- /flac/doc.go: -------------------------------------------------------------------------------- 1 | // Package flac implements audio data decoding in FLAC format. 2 | package flac 3 | -------------------------------------------------------------------------------- /generators/sawtooth.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | 7 | "github.com/gopxl/beep/v2" 8 | ) 9 | 10 | type sawGenerator struct { 11 | dt float64 12 | t float64 13 | 14 | reverse bool 15 | } 16 | 17 | // Creates a streamer which will procude an infinite sawtooth wave with the given frequency. 18 | // use other wrappers of this package to change amplitude or add time limit. 19 | // sampleRate must be at least two times grater then frequency, otherwise this function will return an error. 20 | func SawtoothTone(sr beep.SampleRate, freq float64) (beep.Streamer, error) { 21 | dt := freq / float64(sr) 22 | 23 | if dt >= 1.0/2.0 { 24 | return nil, errors.New("gopxl sawtooth tone generator: samplerate must be at least 2 times grater then frequency") 25 | } 26 | 27 | return &sawGenerator{dt, 0, false}, nil 28 | } 29 | 30 | // Creates a streamer which will procude an infinite sawtooth tone with the given frequency. 31 | // sawtooth is reversed so the slope is negative. 32 | // use other wrappers of this package to change amplitude or add time limit. 33 | // sampleRate must be at least two times grater then frequency, otherwise this function will return an error. 34 | func SawtoothToneReversed(sr beep.SampleRate, freq float64) (beep.Streamer, error) { 35 | dt := freq / float64(sr) 36 | 37 | if dt >= 1.0/2.0 { 38 | return nil, errors.New("gopxl triangle tone generator: samplerate must be at least 2 times grater then frequency") 39 | } 40 | 41 | return &sawGenerator{dt, 0, true}, nil 42 | } 43 | 44 | func (g *sawGenerator) Stream(samples [][2]float64) (n int, ok bool) { 45 | if g.reverse { 46 | for i := range samples { 47 | samples[i][0] = 2.0*(1-g.t) - 1 48 | samples[i][1] = 2.0*(1-g.t) - 1 49 | _, g.t = math.Modf(g.t + g.dt) 50 | } 51 | } else { 52 | for i := range samples { 53 | samples[i][0] = 2.0*g.t - 1.0 54 | samples[i][1] = 2.0*g.t - 1.0 55 | _, g.t = math.Modf(g.t + g.dt) 56 | } 57 | } 58 | 59 | return len(samples), true 60 | } 61 | 62 | func (*sawGenerator) Err() error { 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /generators/silence.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import "github.com/gopxl/beep/v2" 4 | 5 | // Silence returns a Streamer which streams num samples of silence. If num is negative, silence is 6 | // streamed forever. 7 | func Silence(num int) beep.Streamer { 8 | if num < 0 { 9 | return beep.StreamerFunc(func(samples [][2]float64) (m int, ok bool) { 10 | clear(samples) 11 | return len(samples), true 12 | }) 13 | } 14 | 15 | return beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 16 | if num <= 0 { 17 | return 0, false 18 | } 19 | if num < len(samples) { 20 | samples = samples[:num] 21 | } 22 | clear(samples) 23 | num -= len(samples) 24 | 25 | return len(samples), true 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /generators/silence_test.go: -------------------------------------------------------------------------------- 1 | package generators_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/gopxl/beep/v2/generators" 9 | "github.com/gopxl/beep/v2/internal/testtools" 10 | ) 11 | 12 | func TestSilence_StreamsFiniteSamples(t *testing.T) { 13 | s := generators.Silence(100) 14 | 15 | got := testtools.CollectNum(200, s) 16 | assert.Equal(t, make([][2]float64, 100), got) 17 | 18 | got = testtools.CollectNum(200, s) 19 | assert.Len(t, got, 0) 20 | } 21 | 22 | func TestSilence_StreamsInfiniteSamples(t *testing.T) { 23 | s := generators.Silence(-1) 24 | 25 | got := testtools.CollectNum(200, s) 26 | assert.Equal(t, make([][2]float64, 200), got) 27 | 28 | got = testtools.CollectNum(200, s) 29 | assert.Equal(t, make([][2]float64, 200), got) 30 | } 31 | -------------------------------------------------------------------------------- /generators/sine.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | 7 | "github.com/gopxl/beep/v2" 8 | ) 9 | 10 | type sineGenerator struct { 11 | dt float64 12 | t float64 13 | } 14 | 15 | // Creates a streamer which will procude an infinite sine wave with the given frequency. 16 | // use other wrappers of this package to change amplitude or add time limit. 17 | // sampleRate must be at least two times grater then frequency, otherwise this function will return an error. 18 | func SineTone(sr beep.SampleRate, freq float64) (beep.Streamer, error) { 19 | dt := freq / float64(sr) 20 | 21 | if dt >= 1.0/2.0 { 22 | return nil, errors.New("gopxl sine tone generator: samplerate must be at least 2 times grater then frequency") 23 | } 24 | 25 | return &sineGenerator{dt, 0}, nil 26 | } 27 | 28 | func (g *sineGenerator) Stream(samples [][2]float64) (n int, ok bool) { 29 | for i := range samples { 30 | v := math.Sin(g.t * 2.0 * math.Pi) 31 | samples[i][0] = v 32 | samples[i][1] = v 33 | _, g.t = math.Modf(g.t + g.dt) 34 | } 35 | 36 | return len(samples), true 37 | } 38 | 39 | func (*sineGenerator) Err() error { 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /generators/sine_test.go: -------------------------------------------------------------------------------- 1 | package generators_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/gopxl/beep/v2" 9 | "github.com/gopxl/beep/v2/generators" 10 | "github.com/gopxl/beep/v2/internal/testtools" 11 | ) 12 | 13 | func TestSineTone(t *testing.T) { 14 | epsilon := 0.000001 15 | 16 | s, err := generators.SineTone(beep.SampleRate(8000), 400) 17 | assert.NoError(t, err) 18 | 19 | // Get a full single phase including the last sample. 20 | phaseLength := 8000 / 400 21 | samples := testtools.CollectNum(phaseLength+1, s) 22 | 23 | // The sine wave should be 0 at the start, half a phase and at the end of the phase. 24 | assert.InDelta(t, 0, samples[phaseLength*0][0], epsilon) 25 | assert.InDelta(t, 0, samples[phaseLength*0][1], epsilon) 26 | assert.InDelta(t, 0, samples[phaseLength*1/2][0], epsilon) 27 | assert.InDelta(t, 0, samples[phaseLength*1/2][1], epsilon) 28 | assert.InDelta(t, 0, samples[phaseLength*1][0], epsilon) 29 | assert.InDelta(t, 0, samples[phaseLength*1][1], epsilon) 30 | 31 | // The sine wave should be in a peak and trough at 1/4th and 3/4th in the phase respectively. 32 | assert.InDelta(t, 1, samples[phaseLength*1/4][0], epsilon) 33 | assert.InDelta(t, 1, samples[phaseLength*1/4][1], epsilon) 34 | assert.InDelta(t, -1, samples[phaseLength*3/4][0], epsilon) 35 | assert.InDelta(t, -1, samples[phaseLength*3/4][1], epsilon) 36 | } 37 | -------------------------------------------------------------------------------- /generators/square.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | 7 | "github.com/gopxl/beep/v2" 8 | ) 9 | 10 | type squareGenerator struct { 11 | dt float64 12 | t float64 13 | } 14 | 15 | // Creates a streamer which will procude an infinite square wave with the given frequency. 16 | // use other wrappers of this package to change amplitude or add time limit. 17 | // sampleRate must be at least two times grater then frequency, otherwise this function will return an error. 18 | func SquareTone(sr beep.SampleRate, freq float64) (beep.Streamer, error) { 19 | dt := freq / float64(sr) 20 | 21 | if dt >= 1.0/2.0 { 22 | return nil, errors.New("gopxl square tone generator: samplerate must be at least 2 times grater then frequency") 23 | } 24 | 25 | return &squareGenerator{dt, 0}, nil 26 | } 27 | 28 | func (g *squareGenerator) Stream(samples [][2]float64) (n int, ok bool) { 29 | for i := range samples { 30 | if g.t < 0.5 { 31 | samples[i][0] = 1.0 32 | samples[i][1] = 1.0 33 | } else { 34 | samples[i][0] = -1.0 35 | samples[i][1] = -1.0 36 | } 37 | _, g.t = math.Modf(g.t + g.dt) 38 | } 39 | 40 | return len(samples), true 41 | } 42 | 43 | func (*squareGenerator) Err() error { 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /generators/triangle.go: -------------------------------------------------------------------------------- 1 | package generators 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | 7 | "github.com/gopxl/beep/v2" 8 | ) 9 | 10 | type triangleGenerator struct { 11 | dt float64 12 | t float64 13 | } 14 | 15 | // Creates a streamer which will procude an infinite triangle wave with the given frequency. 16 | // use other wrappers of this package to change amplitude or add time limit. 17 | // sampleRate must be at least two times grater then frequency, otherwise this function will return an error. 18 | func TriangleTone(sr beep.SampleRate, freq float64) (beep.Streamer, error) { 19 | dt := freq / float64(sr) 20 | 21 | if dt >= 1.0/2.0 { 22 | return nil, errors.New("gopxl triangle tone generator: samplerate must be at least 2 times grater then frequency") 23 | } 24 | 25 | return &triangleGenerator{dt, 0}, nil 26 | } 27 | 28 | func (g *triangleGenerator) Stream(samples [][2]float64) (n int, ok bool) { 29 | for i := range samples { 30 | if g.t < 0.5 { 31 | samples[i][0] = 2.0*(1-g.t) - 1 32 | samples[i][1] = 2.0*(1-g.t) - 1 33 | } else { 34 | samples[i][0] = 2.0*g.t - 1.0 35 | samples[i][1] = 2.0*g.t - 1.0 36 | } 37 | _, g.t = math.Modf(g.t + g.dt) 38 | } 39 | 40 | return len(samples), true 41 | } 42 | 43 | func (*triangleGenerator) Err() error { 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gopxl/beep/v2 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/ebitengine/oto/v3 v3.3.2 7 | github.com/gdamore/tcell/v2 v2.7.4 8 | github.com/hajimehoshi/go-mp3 v0.3.4 9 | github.com/jfreymuth/oggvorbis v1.0.5 10 | github.com/mewkiz/flac v1.0.12 11 | github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e 12 | github.com/pkg/errors v0.9.1 13 | github.com/samhocevar/go-meltysynth v0.0.0-20230403180939-aca4a036cb16 14 | github.com/stretchr/testify v1.10.0 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/ebitengine/purego v0.8.0 // indirect 20 | github.com/gdamore/encoding v1.0.0 // indirect 21 | github.com/icza/bitio v1.1.0 // indirect 22 | github.com/jfreymuth/vorbis v1.0.2 // indirect 23 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 24 | github.com/mattn/go-runewidth v0.0.15 // indirect 25 | github.com/mewkiz/pkg v0.0.0-20230226050401-4010bf0fec14 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | github.com/rivo/uniseg v0.4.3 // indirect 28 | golang.org/x/sys v0.25.0 // indirect 29 | golang.org/x/term v0.17.0 // indirect 30 | golang.org/x/text v0.14.0 // indirect 31 | gopkg.in/yaml.v3 v3.0.1 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/ebitengine/oto/v3 v3.3.2 h1:VTWBsKX9eb+dXzaF4jEwQbs4yWIdXukJ0K40KgkpYlg= 5 | github.com/ebitengine/oto/v3 v3.3.2/go.mod h1:MZeb/lwoC4DCOdiTIxYezrURTw7EvK/yF863+tmBI+U= 6 | github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= 7 | github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 8 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 9 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 10 | github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= 11 | github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= 12 | github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68= 13 | github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo= 14 | github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo= 15 | github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0= 16 | github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= 17 | github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k= 18 | github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= 19 | github.com/jfreymuth/oggvorbis v1.0.5 h1:u+Ck+R0eLSRhgq8WTmffYnrVtSztJcYrl588DM4e3kQ= 20 | github.com/jfreymuth/oggvorbis v1.0.5/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII= 21 | github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE= 22 | github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ= 23 | github.com/jszwec/csvutil v1.5.1/go.mod h1:Rpu7Uu9giO9subDyMCIQfHVDuLrcaC36UA4YcJjGBkg= 24 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 25 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 26 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 27 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 28 | github.com/mewkiz/flac v1.0.12 h1:5Y1BRlUebfiVXPmz7hDD7h3ceV2XNrGNMejNVjDpgPY= 29 | github.com/mewkiz/flac v1.0.12/go.mod h1:1UeXlFRJp4ft2mfZnPLRpQTd7cSjb/s17o7JQzzyrCA= 30 | github.com/mewkiz/pkg v0.0.0-20230226050401-4010bf0fec14 h1:tnAPMExbRERsyEYkmR1YjhTgDM0iqyiBYf8ojRXxdbA= 31 | github.com/mewkiz/pkg v0.0.0-20230226050401-4010bf0fec14/go.mod h1:QYCFBiH5q6XTHEbWhR0uhR3M9qNPoD2CSQzr0g75kE4= 32 | github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw= 33 | github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= 34 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 35 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 36 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 37 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 40 | github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= 41 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 42 | github.com/samhocevar/go-meltysynth v0.0.0-20230403180939-aca4a036cb16 h1:slzh3BWJ6FyMM8gkDzDwHz+gjU4+82ldB6oPyYi82Ho= 43 | github.com/samhocevar/go-meltysynth v0.0.0-20230403180939-aca4a036cb16/go.mod h1:J+GU4sgu3oAPHCceoTIXNKzFHSybNhF/LyFkWZlqhvE= 44 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 45 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 46 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 47 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 48 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 49 | golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= 50 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 51 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 52 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 53 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 54 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 55 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 56 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 57 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 58 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 59 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 61 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 68 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 69 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 70 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 71 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 72 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 73 | golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= 74 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 75 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 76 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 77 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 78 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 79 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 80 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 81 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 82 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 83 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 84 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 85 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 87 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 88 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 89 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package beep 2 | 3 | // Streamer is able to stream a finite or infinite sequence of audio samples. 4 | type Streamer interface { 5 | // Stream copies at most len(samples) next audio samples to the samples slice. 6 | // 7 | // The sample rate of the samples is unspecified in general, but should be specified for 8 | // each concrete Streamer. 9 | // 10 | // The value at samples[i][0] is the value of the left channel of the i-th sample. 11 | // Similarly, samples[i][1] is the value of the right channel of the i-th sample. 12 | // 13 | // Stream returns the number of streamed samples. If the Streamer is drained and no more 14 | // samples will be produced, it returns 0 and false. Stream must not touch any samples 15 | // outside samples[:n]. 16 | // 17 | // There are 3 valid return patterns of the Stream method: 18 | // 19 | // 1. n == len(samples) && ok 20 | // 21 | // Stream streamed all the requested samples. Cases 1, 2 and 3 may occur in the following 22 | // calls. 23 | // 24 | // 2. 0 < n && n < len(samples) && ok 25 | // 26 | // Stream streamed n samples and drained the Streamer. Only case 3 may occur in the 27 | // following calls. 28 | // 29 | // 3. n == 0 && !ok 30 | // 31 | // The Streamer is drained and no more samples will come. If Err returns a non-nil error, only 32 | // this case is valid. Only this case may occur in the following calls. 33 | Stream(samples [][2]float64) (n int, ok bool) 34 | 35 | // Err returns an error which occurred during streaming. If no error occurred, nil is 36 | // returned. 37 | // 38 | // When an error occurs, Streamer must become drained and Stream must return 0, false 39 | // forever. 40 | // 41 | // The reason why Stream doesn't return an error is that it dramatically simplifies 42 | // programming with Streamer. It's not very important to catch the error right when it 43 | // happens. 44 | Err() error 45 | } 46 | 47 | // StreamSeeker is a finite duration Streamer which supports seeking to an arbitrary position. 48 | type StreamSeeker interface { 49 | Streamer 50 | 51 | // Duration returns the total number of samples of the Streamer. 52 | Len() int 53 | 54 | // Position returns the current position of the Streamer. This value is between 0 and the 55 | // total length. 56 | Position() int 57 | 58 | // Seek sets the position of the Streamer to the provided value. 59 | // 60 | // If an error occurs during seeking, the position remains unchanged. This error will not be 61 | // returned through the Streamer's Err method. 62 | Seek(p int) error 63 | } 64 | 65 | // StreamCloser is a Streamer streaming from a resource which needs to be released, such as a file 66 | // or a network connection. 67 | type StreamCloser interface { 68 | Streamer 69 | 70 | // Close closes the Streamer and releases it's resources. Streamer will no longer stream any 71 | // samples. 72 | Close() error 73 | } 74 | 75 | // StreamSeekCloser is a union of StreamSeeker and StreamCloser. 76 | type StreamSeekCloser interface { 77 | Streamer 78 | Len() int 79 | Position() int 80 | Seek(p int) error 81 | Close() error 82 | } 83 | 84 | // StreamerFunc is a Streamer created by simply wrapping a streaming function (usually a closure, 85 | // which encloses a time tracking variable). This sometimes simplifies creating new streamers. 86 | // 87 | // Example: 88 | // 89 | // noise := StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 90 | // for i := range samples { 91 | // samples[i][0] = rand.Float64()*2 - 1 92 | // samples[i][1] = rand.Float64()*2 - 1 93 | // } 94 | // return len(samples), true 95 | // }) 96 | type StreamerFunc func(samples [][2]float64) (n int, ok bool) 97 | 98 | // Stream calls the wrapped streaming function. 99 | func (sf StreamerFunc) Stream(samples [][2]float64) (n int, ok bool) { 100 | return sf(samples) 101 | } 102 | 103 | // Err always returns nil. 104 | func (sf StreamerFunc) Err() error { 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /internal/testdata/valid_44100hz_22050_samples.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/internal/testdata/valid_44100hz_22050_samples.ogg -------------------------------------------------------------------------------- /internal/testdata/valid_44100hz_22050_samples.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/internal/testdata/valid_44100hz_22050_samples.wav -------------------------------------------------------------------------------- /internal/testdata/valid_44100hz_22050_samples_ffmpeg.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/internal/testdata/valid_44100hz_22050_samples_ffmpeg.flac -------------------------------------------------------------------------------- /internal/testdata/valid_44100hz_x_padded_samples.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gopxl/beep/e2b0015c40fc5a25cd7b2705004274359b2c422b/internal/testdata/valid_44100hz_x_padded_samples.mp3 -------------------------------------------------------------------------------- /internal/testtools/asserts.go: -------------------------------------------------------------------------------- 1 | package testtools 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/gopxl/beep/v2" 10 | ) 11 | 12 | // AssertStreamerHasCorrectReturnBehaviour tests whether the return values returned 13 | // by the streamer s adhere to the description on the Streamer interface. 14 | func AssertStreamerHasCorrectReturnBehaviour(t *testing.T, s beep.Streamer, expectedSamples int) { 15 | const leaveUnreadInFirstCase = 50 16 | 17 | if expectedSamples < leaveUnreadInFirstCase+1 { 18 | panic(fmt.Sprintf("AssertStreamerHasCorrectReturnBehaviour must be called with at least %d samples.", leaveUnreadInFirstCase+1)) 19 | } 20 | 21 | // 1. n == len(samples) && ok 22 | buf := make([][2]float64, 512) 23 | samplesLeft := expectedSamples - leaveUnreadInFirstCase 24 | for samplesLeft > 0 { 25 | toRead := min(samplesLeft, len(buf)) 26 | n, ok := s.Stream(buf[:toRead]) 27 | if !ok { 28 | t.Fatalf("streamer returned !ok before it was expected to be drained") 29 | } 30 | if n < toRead { 31 | t.Fatalf("streamer didn't return all requested samples before it was expected to be drained") 32 | } 33 | if s.Err() != nil { 34 | t.Fatalf("unexpected error in streamer: %v", s.Err()) 35 | } 36 | samplesLeft -= n 37 | } 38 | 39 | // 2. 0 < n && n < len(samples) && ok 40 | n, ok := s.Stream(buf) 41 | assert.True(t, ok) 42 | assert.Equal(t, leaveUnreadInFirstCase, n) 43 | assert.NoError(t, s.Err()) 44 | 45 | // 3. n == 0 && !ok 46 | n, ok = s.Stream(buf) 47 | assert.False(t, ok) 48 | assert.Equal(t, 0, n) 49 | assert.NoError(t, s.Err()) 50 | 51 | // Repeat calls after case 3 must return the same result. 52 | n, ok = s.Stream(buf) 53 | assert.False(t, ok) 54 | assert.Equal(t, 0, n) 55 | assert.NoError(t, s.Err()) 56 | } 57 | 58 | func AssertSamplesEqual(t *testing.T, expected, actual [][2]float64) { 59 | t.Helper() 60 | 61 | if len(expected) != len(actual) { 62 | t.Errorf("expected sample data length to be %d, got %d", len(expected), len(actual)) 63 | return 64 | } 65 | 66 | const epsilon = 1e-9 67 | equals := true 68 | for i := range expected { 69 | if actual[i][0] < expected[i][0]-epsilon || actual[i][0] > expected[i][0]+epsilon || 70 | actual[i][1] < expected[i][1]-epsilon || actual[i][1] > expected[i][1]+epsilon { 71 | equals = false 72 | break 73 | } 74 | } 75 | if !equals { 76 | t.Errorf("the sample data isn't equal to the expected data") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/testtools/sinks.go: -------------------------------------------------------------------------------- 1 | package testtools 2 | 3 | import "github.com/gopxl/beep/v2" 4 | 5 | // Collect drains Streamer s and returns all the samples it streamed. 6 | func Collect(s beep.Streamer) [][2]float64 { 7 | var ( 8 | result [][2]float64 9 | buf [479][2]float64 10 | ) 11 | for { 12 | n, ok := s.Stream(buf[:]) 13 | if !ok { 14 | return result 15 | } 16 | result = append(result, buf[:n]...) 17 | } 18 | } 19 | 20 | // CollectNum collects num samples from the streamer in chunks and returns them all. 21 | func CollectNum(num int, s beep.Streamer) [][2]float64 { 22 | return Collect(beep.Take(num, s)) 23 | } 24 | -------------------------------------------------------------------------------- /internal/testtools/streamers.go: -------------------------------------------------------------------------------- 1 | package testtools 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/gopxl/beep/v2" 7 | ) 8 | 9 | // RandomDataStreamer generates numSamples random samples and returns a StreamSeeker to stream them. 10 | func RandomDataStreamer(numSamples int) (s beep.StreamSeeker, data [][2]float64) { 11 | data = make([][2]float64, numSamples) 12 | for i := range data { 13 | data[i] = [2]float64{ 14 | rand.Float64()*2 - 1, 15 | rand.Float64()*2 - 1, 16 | } 17 | } 18 | return NewDataStreamer(data), data 19 | } 20 | 21 | // NewSequentialDataStreamer creates a streamer which streams samples with values {0, 0}, {1, 1}, {2, 2}, etc. 22 | // Note that this aren't valid sample values in the range of [-1, 1], but it can nonetheless 23 | // be useful for testing. 24 | func NewSequentialDataStreamer(numSamples int) (s beep.StreamSeeker, data [][2]float64) { 25 | data = make([][2]float64, numSamples) 26 | for i := range data { 27 | data[i] = [2]float64{float64(i), float64(i)} 28 | } 29 | return NewDataStreamer(data), data 30 | } 31 | 32 | // NewDataStreamer creates a streamer which streams the given data. 33 | func NewDataStreamer(data [][2]float64) (s beep.StreamSeeker) { 34 | return &dataStreamer{data, 0} 35 | } 36 | 37 | type dataStreamer struct { 38 | data [][2]float64 39 | pos int 40 | } 41 | 42 | func (ds *dataStreamer) Stream(samples [][2]float64) (n int, ok bool) { 43 | if ds.pos >= len(ds.data) { 44 | return 0, false 45 | } 46 | n = copy(samples, ds.data[ds.pos:]) 47 | ds.pos += n 48 | return n, true 49 | } 50 | 51 | func (ds *dataStreamer) Err() error { 52 | return nil 53 | } 54 | 55 | func (ds *dataStreamer) Len() int { 56 | return len(ds.data) 57 | } 58 | 59 | func (ds *dataStreamer) Position() int { 60 | return ds.pos 61 | } 62 | 63 | func (ds *dataStreamer) Seek(p int) error { 64 | ds.pos = p 65 | return nil 66 | } 67 | 68 | // NewErrorStreamer returns a streamer which errors immediately with the given err. 69 | func NewErrorStreamer(err error) beep.StreamSeeker { 70 | return &ErrorStreamer{ 71 | s: beep.StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 72 | panic("unreachable") 73 | }), 74 | samplesLeft: 0, 75 | Error: err, 76 | } 77 | } 78 | 79 | // NewDelayedErrorStreamer wraps streamer s but returns an error after numSamples have been streamed. 80 | func NewDelayedErrorStreamer(s beep.Streamer, numSamples int, err error) beep.StreamSeeker { 81 | return &ErrorStreamer{ 82 | s: s, 83 | samplesLeft: numSamples, 84 | Error: err, 85 | } 86 | } 87 | 88 | type ErrorStreamer struct { 89 | s beep.Streamer 90 | samplesLeft int 91 | Error error 92 | } 93 | 94 | func (e *ErrorStreamer) Stream(samples [][2]float64) (n int, ok bool) { 95 | if e.samplesLeft == 0 { 96 | return 0, false 97 | } 98 | 99 | toStream := min(e.samplesLeft, len(samples)) 100 | n, ok = e.s.Stream(samples[:toStream]) 101 | e.samplesLeft -= n 102 | 103 | return n, ok 104 | } 105 | 106 | func (e *ErrorStreamer) Err() error { 107 | if e.samplesLeft == 0 { 108 | return e.Error 109 | } else { 110 | return e.s.Err() 111 | } 112 | } 113 | 114 | func (e *ErrorStreamer) Seek(p int) error { 115 | if s, ok := e.s.(beep.StreamSeeker); ok { 116 | return s.Seek(p) 117 | } 118 | panic("source streamer is not a beep.StreamSeeker") 119 | } 120 | 121 | func (e *ErrorStreamer) Len() int { 122 | if s, ok := e.s.(beep.StreamSeeker); ok { 123 | return s.Len() 124 | } 125 | panic("source streamer is not a beep.StreamSeeker") 126 | } 127 | 128 | func (e *ErrorStreamer) Position() int { 129 | if s, ok := e.s.(beep.StreamSeeker); ok { 130 | return s.Position() 131 | } 132 | panic("source streamer is not a beep.StreamSeeker") 133 | } 134 | 135 | func NewSeekErrorStreamer(s beep.StreamSeeker, err error) *SeekErrorStreamer { 136 | return &SeekErrorStreamer{ 137 | s: s, 138 | err: err, 139 | } 140 | } 141 | 142 | type SeekErrorStreamer struct { 143 | s beep.StreamSeeker 144 | err error 145 | } 146 | 147 | func (s *SeekErrorStreamer) Stream(samples [][2]float64) (n int, ok bool) { 148 | return s.s.Stream(samples) 149 | } 150 | 151 | func (s *SeekErrorStreamer) Err() error { 152 | return s.s.Err() 153 | } 154 | 155 | func (s *SeekErrorStreamer) Len() int { 156 | return s.s.Len() 157 | } 158 | 159 | func (s *SeekErrorStreamer) Position() int { 160 | return s.s.Position() 161 | } 162 | 163 | func (s *SeekErrorStreamer) Seek(p int) error { 164 | return s.err 165 | } 166 | -------------------------------------------------------------------------------- /internal/testtools/testdata.go: -------------------------------------------------------------------------------- 1 | package testtools 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | // See https://stackoverflow.com/a/38644571 10 | _, callerPath, _, _ = runtime.Caller(0) 11 | TestDataDir = filepath.Join(filepath.Dir(callerPath), "../testdata") 12 | ) 13 | 14 | func TestFilePath(path string) string { 15 | return filepath.Join(TestDataDir, path) 16 | } 17 | -------------------------------------------------------------------------------- /internal/util/math.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "cmp" 4 | 5 | func Clamp[T cmp.Ordered](x, minV, maxV T) T { 6 | return max(min(x, maxV), minV) 7 | } 8 | -------------------------------------------------------------------------------- /internal/util/math_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestClamp(t *testing.T) { 10 | assert.Equal(t, 0, Clamp(-5, 0, 1)) 11 | assert.Equal(t, 1, Clamp(5, 0, 1)) 12 | assert.Equal(t, 0.5, Clamp(0.5, 0, 1)) 13 | } 14 | -------------------------------------------------------------------------------- /midi/decode.go: -------------------------------------------------------------------------------- 1 | // Package midi implements audio data decoding in MIDI format. 2 | package midi 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/samhocevar/go-meltysynth/meltysynth" 10 | 11 | "github.com/gopxl/beep/v2" 12 | ) 13 | 14 | const ( 15 | midiNumChannels = 2 16 | midiPrecision = 4 17 | ) 18 | 19 | // NewSoundFont reads a sound font containing instruments. A sound font is required in order to play MIDI files. 20 | // 21 | // NewSoundFont closes the supplied ReadCloser. 22 | func NewSoundFont(r io.ReadCloser) (*SoundFont, error) { 23 | sf, err := meltysynth.NewSoundFont(r) 24 | if err != nil { 25 | return nil, err 26 | } 27 | err = r.Close() 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &SoundFont{sf}, nil 32 | } 33 | 34 | type SoundFont struct { 35 | sf *meltysynth.SoundFont 36 | } 37 | 38 | // Decode takes a ReadCloser containing audio data in MIDI format and a SoundFont to synthesize the sounds 39 | // and returns a beep.StreamSeeker, which streams the audio. 40 | // 41 | // Decode closes the supplied ReadCloser. 42 | func Decode(rc io.ReadCloser, sf *SoundFont, sampleRate beep.SampleRate) (s beep.StreamSeeker, format beep.Format, err error) { 43 | defer func() { 44 | if err != nil { 45 | err = errors.Wrap(err, "midi") 46 | } 47 | }() 48 | 49 | settings := meltysynth.NewSynthesizerSettings(int32(sampleRate)) 50 | synth, err := meltysynth.NewSynthesizer(sf.sf, settings) 51 | if err != nil { 52 | return nil, beep.Format{}, err 53 | } 54 | 55 | mf, err := meltysynth.NewMidiFile(rc) 56 | if err != nil { 57 | return nil, beep.Format{}, err 58 | } 59 | err = rc.Close() 60 | if err != nil { 61 | return nil, beep.Format{}, err 62 | } 63 | 64 | seq := meltysynth.NewMidiFileSequencer(synth) 65 | seq.Play(mf /*loop*/, false) 66 | 67 | format = beep.Format{ 68 | SampleRate: sampleRate, 69 | NumChannels: midiNumChannels, 70 | Precision: midiPrecision, 71 | } 72 | 73 | return &decoder{ 74 | synth: synth, 75 | mf: mf, 76 | seq: seq, 77 | sampleRate: sampleRate, 78 | bufLeft: make([]float32, 512), 79 | bufRight: make([]float32, 512), 80 | }, format, nil 81 | } 82 | 83 | type decoder struct { 84 | synth *meltysynth.Synthesizer 85 | mf *meltysynth.MidiFile 86 | seq *meltysynth.MidiFileSequencer 87 | sampleRate beep.SampleRate 88 | bufLeft, bufRight []float32 89 | err error 90 | } 91 | 92 | func (d *decoder) Stream(samples [][2]float64) (n int, ok bool) { 93 | if d.err != nil { 94 | return 0, false 95 | } 96 | 97 | samplesLeft := d.Len() - d.Position() 98 | if len(samples) > samplesLeft { 99 | samples = samples[:samplesLeft] 100 | } 101 | 102 | for len(samples) > 0 { 103 | cn := min(len(d.bufLeft), len(samples)) 104 | 105 | d.seq.Render(d.bufLeft[:cn], d.bufRight[:cn]) 106 | for i := 0; i < cn; i++ { 107 | samples[i][0] = float64(d.bufLeft[i]) 108 | samples[i][1] = float64(d.bufRight[i]) 109 | } 110 | 111 | samples = samples[cn:] 112 | n += cn 113 | } 114 | 115 | return n, n > 0 116 | } 117 | 118 | func (d *decoder) Err() error { 119 | return d.err 120 | } 121 | 122 | func (d *decoder) Len() int { 123 | return d.sampleRate.N(d.mf.GetLength()) 124 | } 125 | 126 | func (d *decoder) Position() int { 127 | return d.sampleRate.N(d.seq.Pos()) 128 | } 129 | 130 | func (d *decoder) Seek(p int) error { 131 | if p < 0 || d.Len() < p { 132 | return fmt.Errorf("midi: seek position %v out of range [%v, %v]", p, 0, d.Len()) 133 | } 134 | d.seq.Seek(d.sampleRate.D(p)) 135 | return nil 136 | } 137 | -------------------------------------------------------------------------------- /mixer.go: -------------------------------------------------------------------------------- 1 | package beep 2 | 3 | // Mixer allows for dynamic mixing of arbitrary number of Streamers. Mixer automatically removes 4 | // drained Streamers. Depending on the KeepAlive() setting, Stream will either play silence or 5 | // drain when all Streamers have been drained. By default, Mixer keeps playing silence. 6 | type Mixer struct { 7 | streamers []Streamer 8 | stopWhenEmpty bool 9 | } 10 | 11 | // KeepAlive configures the Mixer to either keep playing silence when all its Streamers have 12 | // drained (keepAlive == true) or stop playing (keepAlive == false). 13 | func (m *Mixer) KeepAlive(keepAlive bool) { 14 | m.stopWhenEmpty = !keepAlive 15 | } 16 | 17 | // Len returns the number of Streamers currently playing in the Mixer. 18 | func (m *Mixer) Len() int { 19 | return len(m.streamers) 20 | } 21 | 22 | // Add adds Streamers to the Mixer. 23 | func (m *Mixer) Add(s ...Streamer) { 24 | m.streamers = append(m.streamers, s...) 25 | } 26 | 27 | // Clear removes all Streamers from the mixer. 28 | func (m *Mixer) Clear() { 29 | for i := range m.streamers { 30 | m.streamers[i] = nil 31 | } 32 | m.streamers = m.streamers[:0] 33 | } 34 | 35 | // Stream the samples of all Streamers currently in the Mixer mixed together. Depending on the 36 | // KeepAlive() setting, Stream will either play silence or drain when all Streamers have been 37 | // drained. 38 | func (m *Mixer) Stream(samples [][2]float64) (n int, ok bool) { 39 | if m.stopWhenEmpty && len(m.streamers) == 0 { 40 | return 0, false 41 | } 42 | 43 | var tmp [512][2]float64 44 | 45 | for len(samples) > 0 { 46 | toStream := min(len(tmp), len(samples)) 47 | 48 | // Clear the samples 49 | clear(samples[:toStream]) 50 | 51 | snMax := 0 52 | for si := 0; si < len(m.streamers); si++ { 53 | // Mix the stream 54 | sn, sok := m.streamers[si].Stream(tmp[:toStream]) 55 | for i := range tmp[:sn] { 56 | samples[i][0] += tmp[i][0] 57 | samples[i][1] += tmp[i][1] 58 | } 59 | if sn > snMax { 60 | snMax = sn 61 | } 62 | 63 | if sn < toStream || !sok { 64 | // Remove drained streamer. 65 | // Check the length of m.streamers again in case the call to Stream() 66 | // had a callback which clears the Mixer. 67 | if len(m.streamers) > 0 { 68 | last := len(m.streamers) - 1 69 | m.streamers[si] = m.streamers[last] 70 | m.streamers[last] = nil 71 | m.streamers = m.streamers[:last] 72 | si-- 73 | } 74 | 75 | if m.stopWhenEmpty && len(m.streamers) == 0 { 76 | return n + snMax, true 77 | } 78 | } 79 | } 80 | 81 | samples = samples[toStream:] 82 | n += toStream 83 | } 84 | 85 | return n, true 86 | } 87 | 88 | // Err always returns nil for Mixer. 89 | // 90 | // There are two reasons. The first one is that erroring Streamers are immediately drained and 91 | // removed from the Mixer. The second one is that one Streamer shouldn't break the whole Mixer and 92 | // you should handle the errors right where they can happen. 93 | func (m *Mixer) Err() error { 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /mixer_test.go: -------------------------------------------------------------------------------- 1 | package beep_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/gopxl/beep/v2" 9 | "github.com/gopxl/beep/v2/internal/testtools" 10 | ) 11 | 12 | func TestMixer_MixesSamples(t *testing.T) { 13 | epsilon := 0.000001 14 | 15 | s1, data1 := testtools.RandomDataStreamer(200) 16 | s2, data2 := testtools.RandomDataStreamer(200) 17 | 18 | m := beep.Mixer{} 19 | m.Add(s1) 20 | m.Add(s2) 21 | 22 | samples := testtools.CollectNum(100, &m) 23 | for i, s := range samples { 24 | wantL := data1[i][0] + data2[i][0] 25 | wantR := data1[i][1] + data2[i][1] 26 | 27 | if s[0] < wantL-epsilon || s[0] > wantL+epsilon { 28 | t.Fatalf("unexpected value for mixed samples; expected: %f, got: %f", wantL, s[0]) 29 | } 30 | if s[1] < wantR-epsilon || s[1] > wantR+epsilon { 31 | t.Fatalf("unexpected value for mixed samples; expected: %f, got: %f", wantR, s[1]) 32 | } 33 | } 34 | 35 | s3, data3 := testtools.RandomDataStreamer(100) 36 | m.Add(s3) 37 | 38 | samples = testtools.CollectNum(100, &m) 39 | for i, s := range samples { 40 | wantL := data1[100+i][0] + data2[100+i][0] + data3[i][0] 41 | wantR := data1[100+i][1] + data2[100+i][1] + data3[i][1] 42 | 43 | if s[0] < wantL-epsilon || s[0] > wantL+epsilon { 44 | t.Fatalf("unexpected value for mixed samples; expected: %f, got: %f", wantL, s[0]) 45 | } 46 | if s[1] < wantR-epsilon || s[1] > wantR+epsilon { 47 | t.Fatalf("unexpected value for mixed samples; expected: %f, got: %f", wantR, s[1]) 48 | } 49 | } 50 | } 51 | 52 | func TestMixer_DrainedStreamersAreRemoved(t *testing.T) { 53 | s1, _ := testtools.RandomDataStreamer(50) 54 | s2, _ := testtools.RandomDataStreamer(65) 55 | 56 | m := beep.Mixer{} 57 | m.Add(s1) 58 | m.Add(s2) 59 | 60 | // Drain s1 but not so far it returns false. 61 | samples := testtools.CollectNum(50, &m) 62 | assert.Len(t, samples, 50) 63 | assert.Equal(t, 2, m.Len()) 64 | 65 | // Drain s1 (s1 returns !ok, n == 0) 66 | samples = testtools.CollectNum(10, &m) 67 | assert.Len(t, samples, 10) 68 | assert.Equal(t, 1, m.Len()) 69 | 70 | // Drain s2 (s2 returns ok, n < len(samples)) 71 | samples = testtools.CollectNum(10, &m) 72 | assert.Len(t, samples, 10) 73 | assert.Equal(t, 0, m.Len()) 74 | } 75 | 76 | func TestMixer_PlaysSilenceWhenNoStreamersProduceSamples(t *testing.T) { 77 | m := beep.Mixer{} 78 | 79 | // Test silence before streamers are added. 80 | samples := testtools.CollectNum(10, &m) 81 | assert.Len(t, samples, 10) 82 | assert.Equal(t, make([][2]float64, 10), samples) 83 | 84 | // Test silence after streamer has only streamed part of the requested samples. 85 | s, data := testtools.RandomDataStreamer(50) 86 | m.Add(s) 87 | samples = testtools.CollectNum(100, &m) 88 | assert.Len(t, samples, 100) 89 | assert.Equal(t, 0, m.Len()) 90 | assert.Equal(t, data, samples[:50]) 91 | assert.Equal(t, make([][2]float64, 50), samples[50:]) 92 | 93 | // Test silence after streamers have been drained & removed. 94 | samples = testtools.CollectNum(10, &m) 95 | assert.Len(t, samples, 10) 96 | assert.Equal(t, make([][2]float64, 10), samples) 97 | } 98 | 99 | // Bug: https://github.com/gopxl/beep/issues/197#issuecomment-2468951277 100 | func TestMixer_CanClearDuringCallback(t *testing.T) { 101 | m := beep.Mixer{} 102 | 103 | m.Add(beep.Callback(func() { 104 | m.Clear() 105 | })) 106 | 107 | testtools.CollectNum(10, &m) 108 | } 109 | 110 | func BenchmarkMixer_MultipleStreams(b *testing.B) { 111 | s1, _ := testtools.RandomDataStreamer(b.N) 112 | s2, _ := testtools.RandomDataStreamer(b.N) 113 | 114 | m := beep.Mixer{} 115 | m.Add(s1) 116 | m.Add(s2) 117 | 118 | b.StartTimer() 119 | 120 | testtools.CollectNum(b.N, &m) 121 | } 122 | 123 | func BenchmarkMixer_OneStream(b *testing.B) { 124 | s, _ := testtools.RandomDataStreamer(b.N) 125 | 126 | m := beep.Mixer{} 127 | m.Add(s) 128 | 129 | b.StartTimer() 130 | testtools.CollectNum(b.N, &m) 131 | } 132 | 133 | func BenchmarkMixer_Silence(b *testing.B) { 134 | m := beep.Mixer{} 135 | // Don't add any streamers 136 | 137 | b.StartTimer() 138 | testtools.CollectNum(b.N, &m) 139 | } 140 | -------------------------------------------------------------------------------- /mp3/decode.go: -------------------------------------------------------------------------------- 1 | // Package mp3 implements audio data decoding in MP3 format. 2 | package mp3 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | 8 | gomp3 "github.com/hajimehoshi/go-mp3" 9 | "github.com/pkg/errors" 10 | 11 | "github.com/gopxl/beep/v2" 12 | ) 13 | 14 | const ( 15 | gomp3NumChannels = 2 16 | gomp3Precision = 2 17 | gomp3BytesPerFrame = gomp3NumChannels * gomp3Precision 18 | ) 19 | 20 | // Decode takes a ReadCloser containing audio data in MP3 format and returns a StreamSeekCloser, 21 | // which streams that audio. The Seek method will panic if rc is not io.Seeker. 22 | // 23 | // Do not close the supplied ReadSeekCloser, instead, use the Close method of the returned 24 | // StreamSeekCloser when you want to release the resources. 25 | func Decode(rc io.ReadCloser) (s beep.StreamSeekCloser, format beep.Format, err error) { 26 | defer func() { 27 | if err != nil { 28 | err = errors.Wrap(err, "mp3") 29 | } 30 | }() 31 | d, err := gomp3.NewDecoder(rc) 32 | if err != nil { 33 | return nil, beep.Format{}, err 34 | } 35 | format = beep.Format{ 36 | SampleRate: beep.SampleRate(d.SampleRate()), 37 | NumChannels: gomp3NumChannels, 38 | Precision: gomp3Precision, 39 | } 40 | return &decoder{rc, d, format, 0, nil}, format, nil 41 | } 42 | 43 | type decoder struct { 44 | closer io.Closer 45 | d *gomp3.Decoder 46 | f beep.Format 47 | pos int 48 | err error 49 | } 50 | 51 | func (d *decoder) Stream(samples [][2]float64) (n int, ok bool) { 52 | if d.err != nil { 53 | return 0, false 54 | } 55 | var tmp [gomp3BytesPerFrame]byte 56 | for i := range samples { 57 | dn, err := d.d.Read(tmp[:]) 58 | if dn == len(tmp) { 59 | samples[i], _ = d.f.DecodeSigned(tmp[:]) 60 | d.pos += dn 61 | n++ 62 | ok = true 63 | } 64 | if err == io.EOF { 65 | break 66 | } 67 | if err != nil { 68 | d.err = errors.Wrap(err, "mp3") 69 | break 70 | } 71 | } 72 | return n, ok 73 | } 74 | 75 | func (d *decoder) Err() error { 76 | return d.err 77 | } 78 | 79 | func (d *decoder) Len() int { 80 | return int(d.d.Length()) / gomp3BytesPerFrame 81 | } 82 | 83 | func (d *decoder) Position() int { 84 | return d.pos / gomp3BytesPerFrame 85 | } 86 | 87 | func (d *decoder) Seek(p int) error { 88 | if p < 0 || d.Len() < p { 89 | return fmt.Errorf("mp3: seek position %v out of range [%v, %v]", p, 0, d.Len()) 90 | } 91 | _, err := d.d.Seek(int64(p)*gomp3BytesPerFrame, io.SeekStart) 92 | if err != nil { 93 | return errors.Wrap(err, "mp3") 94 | } 95 | d.pos = p * gomp3BytesPerFrame 96 | return nil 97 | } 98 | 99 | func (d *decoder) Close() error { 100 | err := d.closer.Close() 101 | if err != nil { 102 | return errors.Wrap(err, "mp3") 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /mp3/decode_test.go: -------------------------------------------------------------------------------- 1 | package mp3_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/gopxl/beep/v2/internal/testtools" 10 | "github.com/gopxl/beep/v2/mp3" 11 | ) 12 | 13 | func TestDecoder_ReturnBehaviour(t *testing.T) { 14 | f, err := os.Open(testtools.TestFilePath("valid_44100hz_x_padded_samples.mp3")) 15 | assert.NoError(t, err) 16 | defer f.Close() 17 | 18 | s, _, err := mp3.Decode(f) 19 | assert.NoError(t, err) 20 | // The length of the streamer isn't tested because mp3 files have 21 | // a different padding depending on the decoder used. 22 | // https://superuser.com/a/1393775 23 | 24 | testtools.AssertStreamerHasCorrectReturnBehaviour(t, s, s.Len()) 25 | } 26 | -------------------------------------------------------------------------------- /resample.go: -------------------------------------------------------------------------------- 1 | package beep 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | const resamplerSingleBufferSize = 512 9 | 10 | // Resample takes a Streamer which is assumed to stream at the old sample rate and returns a 11 | // Streamer, which streams the data from the original Streamer resampled to the new sample rate. 12 | // 13 | // This is, for example, useful when mixing multiple Streamer with different sample rates, either 14 | // through a beep.Mixer, or through a speaker. Speaker has a constant sample rate. Thus, playing 15 | // Streamer which stream at a different sample rate will lead to a changed speed and pitch of the 16 | // playback. 17 | // 18 | // sr := beep.SampleRate(48000) 19 | // speaker.Init(sr, sr.N(time.Second/2)) 20 | // speaker.Play(beep.Resample(3, format.SampleRate, sr, s)) 21 | // 22 | // In the example above, the original sample rate of the source is format.SampleRate. We want to play 23 | // it at the speaker's native sample rate and thus we need to resample. 24 | // 25 | // The quality argument specifies the quality of the resampling process. Higher quality implies 26 | // worse performance. Values below 1 or above 64 are invalid and Resample will panic. Here's a table 27 | // for deciding which quality to pick. 28 | // 29 | // quality | use case 30 | // --------|--------- 31 | // 1 | very high performance, on-the-fly resampling, low quality 32 | // 3-4 | good performance, on-the-fly resampling, good quality 33 | // 6 | higher CPU usage, usually not suitable for on-the-fly resampling, very good quality 34 | // >6 | even higher CPU usage, for offline resampling, very good quality 35 | // 36 | // Sane quality values are usually below 16. Higher values will consume too much CPU, giving 37 | // negligible quality improvements. 38 | // 39 | // Resample propagates errors from s. 40 | func Resample(quality int, old, new SampleRate, s Streamer) *Resampler { 41 | return ResampleRatio(quality, float64(old)/float64(new), s) 42 | } 43 | 44 | // ResampleRatio is same as Resample, except it takes the ratio of the old and the new sample rate, 45 | // specifically, the old sample rate divided by the new sample rate. Aside from correcting the 46 | // sample rate, this can be used to change the speed of the audio. For example, resampling at the 47 | // ratio of 2 and playing at the original sample rate will cause doubled speed in playback. 48 | func ResampleRatio(quality int, ratio float64, s Streamer) *Resampler { 49 | if quality < 1 || 64 < quality { 50 | panic(fmt.Errorf("resample: invalid quality: %d", quality)) 51 | } 52 | if ratio <= 0 || math.IsInf(ratio, 0) || math.IsNaN(ratio) { 53 | panic(fmt.Errorf("resample: invalid ratio: %f", ratio)) 54 | } 55 | return &Resampler{ 56 | s: s, 57 | ratio: ratio, 58 | buf1: make([][2]float64, resamplerSingleBufferSize), 59 | buf2: make([][2]float64, resamplerSingleBufferSize), 60 | pts: make([]point, quality*2), 61 | // The initial value of `off` is set so that the current position is just behind the end 62 | // of buf2: 63 | // current position (0) - len(buf2) = -resamplerSingleBufferSize 64 | // When the Stream() method is called for the first time, it will determine that neither 65 | // buf1 nor buf2 contain the required samples because they are both in the past relative to 66 | // the chosen `off` value. As a result, buf2 will be filled with samples, and `off` will be 67 | // incremented by resamplerSingleBufferSize, making `off` equal to 0. This will align the 68 | // start of buf2 with the current position. 69 | off: -resamplerSingleBufferSize, 70 | pos: 0.0, 71 | end: math.MaxInt, 72 | } 73 | } 74 | 75 | // Resampler is a Streamer created by Resample and ResampleRatio functions. It allows dynamic 76 | // changing of the resampling ratio, which can be useful for dynamically changing the speed of 77 | // streaming. 78 | type Resampler struct { 79 | s Streamer // the original streamer 80 | ratio float64 // old sample rate / new sample rate 81 | buf1, buf2 [][2]float64 // buf1 contains previous buf2, new data goes into buf2, buf1 is because interpolation might require old samples 82 | pts []point // pts is for points used for interpolation 83 | off int // off is the position of the start of buf2 in the original data 84 | pos float64 // pos is the current position in the resampled data 85 | end int // end is the position after the last sample in the original data 86 | } 87 | 88 | // Stream streams the original audio resampled according to the current ratio. 89 | func (r *Resampler) Stream(samples [][2]float64) (n int, ok bool) { 90 | for len(samples) > 0 { 91 | // Calculate the current position in the original data. 92 | wantPos := r.pos * r.ratio 93 | 94 | // Determine the quality*2 closest sample positions for the interpolation. 95 | // The window has length len(r.pts) and is centered around wantPos. 96 | windowStart := int(wantPos) - (len(r.pts)-1)/2 // (inclusive) 97 | windowEnd := int(wantPos) + len(r.pts)/2 + 1 // (exclusive) 98 | 99 | // Prepare the buffers. 100 | for windowEnd > r.off+resamplerSingleBufferSize { 101 | // We load into buf1. 102 | sn, _ := r.s.Stream(r.buf1) 103 | if sn < len(r.buf1) { 104 | r.end = r.off + resamplerSingleBufferSize + sn 105 | } 106 | 107 | // Swap buffers. 108 | r.buf1, r.buf2 = r.buf2, r.buf1 109 | r.off += resamplerSingleBufferSize 110 | } 111 | 112 | // Exit when wantPos is after the end of the original data. 113 | if int(wantPos) >= r.end { 114 | return n, n > 0 115 | } 116 | 117 | // Adjust the window to be within the available buffers. 118 | windowStart = max(windowStart, 0) 119 | windowEnd = min(windowEnd, r.end) 120 | 121 | // For each channel... 122 | for c := range samples[0] { 123 | // Get the points. 124 | numPts := windowEnd - windowStart 125 | pts := r.pts[:numPts] 126 | for i := range pts { 127 | x := windowStart + i 128 | var y float64 129 | if x < r.off { 130 | // Sample is in buf1. 131 | offBuf1 := r.off - resamplerSingleBufferSize 132 | y = r.buf1[x-offBuf1][c] 133 | } else { 134 | // Sample is in buf2. 135 | y = r.buf2[x-r.off][c] 136 | } 137 | pts[i] = point{ 138 | X: float64(x), 139 | Y: y, 140 | } 141 | } 142 | 143 | // Calculate the resampled sample using polynomial interpolation from the 144 | // quality*2 closest samples. 145 | samples[0][c] = lagrange(pts, wantPos) 146 | } 147 | 148 | samples = samples[1:] 149 | n++ 150 | r.pos++ 151 | } 152 | 153 | return n, true 154 | } 155 | 156 | // Err propagates the original Streamer's errors. 157 | func (r *Resampler) Err() error { 158 | return r.s.Err() 159 | } 160 | 161 | // Ratio returns the current resampling ratio. 162 | func (r *Resampler) Ratio() float64 { 163 | return r.ratio 164 | } 165 | 166 | // SetRatio sets the resampling ratio. This does not cause any glitches in the stream. 167 | func (r *Resampler) SetRatio(ratio float64) { 168 | if ratio <= 0 || math.IsInf(ratio, 0) || math.IsNaN(ratio) { 169 | panic(fmt.Errorf("resample: invalid ratio: %f", ratio)) 170 | } 171 | r.pos *= r.ratio / ratio 172 | r.ratio = ratio 173 | } 174 | 175 | // lagrange calculates the value at x of a polynomial of order len(pts)+1 which goes through all 176 | // points in pts 177 | func lagrange(pts []point, x float64) (y float64) { 178 | y = 0.0 179 | for j := range pts { 180 | l := 1.0 181 | for m := range pts { 182 | if j == m { 183 | continue 184 | } 185 | l *= (x - pts[m].X) / (pts[j].X - pts[m].X) 186 | } 187 | y += pts[j].Y * l 188 | } 189 | return y 190 | } 191 | 192 | type point struct { 193 | X, Y float64 194 | } 195 | -------------------------------------------------------------------------------- /resample_test.go: -------------------------------------------------------------------------------- 1 | package beep_test 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/gopxl/beep/v2" 9 | "github.com/gopxl/beep/v2/internal/testtools" 10 | ) 11 | 12 | func TestResample(t *testing.T) { 13 | for _, numSamples := range []int{8, 100, 500, 1000, 50000} { 14 | for _, old := range []beep.SampleRate{100, 2000, 44100, 48000} { 15 | for _, new := range []beep.SampleRate{100, 2000, 44100, 48000} { 16 | if numSamples/int(old)*int(new) > 1e6 { 17 | continue // skip too expensive combinations 18 | } 19 | 20 | t.Run(fmt.Sprintf("numSamples_%d_old_%d_new_%d", numSamples, old, new), func(t *testing.T) { 21 | s, data := testtools.RandomDataStreamer(numSamples) 22 | 23 | want := resampleCorrect(3, old, new, data) 24 | 25 | got := testtools.Collect(beep.Resample(3, old, new, s)) 26 | 27 | if !reflect.DeepEqual(want, got) { 28 | t.Fatal("Resample not working correctly") 29 | } 30 | }) 31 | } 32 | } 33 | } 34 | } 35 | 36 | func resampleCorrect(quality int, old, new beep.SampleRate, p [][2]float64) [][2]float64 { 37 | ratio := float64(old) / float64(new) 38 | pts := make([]point, quality*2) 39 | var resampled [][2]float64 40 | for i := 0; ; i++ { 41 | j := float64(i) * ratio 42 | if int(j) >= len(p) { 43 | break 44 | } 45 | var sample [2]float64 46 | for c := range sample { 47 | for k := range pts { 48 | l := int(j) + k - (len(pts)-1)/2 49 | if l >= 0 && l < len(p) { 50 | pts[k] = point{X: float64(l), Y: p[l][c]} 51 | } else { 52 | pts[k] = point{X: float64(l), Y: 0} 53 | } 54 | } 55 | 56 | startK := 0 57 | for k, pt := range pts { 58 | if pt.X >= 0 { 59 | startK = k 60 | break 61 | } 62 | } 63 | endK := 0 64 | for k, pt := range pts { 65 | if pt.X < float64(len(p)) { 66 | endK = k + 1 67 | } 68 | } 69 | 70 | y := lagrange(pts[startK:endK], j) 71 | sample[c] = y 72 | } 73 | resampled = append(resampled, sample) 74 | } 75 | return resampled 76 | } 77 | 78 | func lagrange(pts []point, x float64) (y float64) { 79 | y = 0.0 80 | for j := range pts { 81 | l := 1.0 82 | for m := range pts { 83 | if j == m { 84 | continue 85 | } 86 | l *= (x - pts[m].X) / (pts[j].X - pts[m].X) 87 | } 88 | y += pts[j].Y * l 89 | } 90 | return y 91 | } 92 | 93 | type point struct { 94 | X, Y float64 95 | } 96 | 97 | func FuzzResampler_SetRatio(f *testing.F) { 98 | f.Add(44100, 48000, 0.5, 1.0, 8.0) 99 | f.Fuzz(func(t *testing.T, original, desired int, r1, r2, r3 float64) { 100 | if original <= 0 || desired <= 0 || r1 <= 0 || r2 <= 0 || r3 <= 0 { 101 | t.Skip() 102 | } 103 | 104 | s, _ := testtools.RandomDataStreamer(1e4) 105 | r := beep.Resample(4, beep.SampleRate(original), beep.SampleRate(desired), s) 106 | testtools.CollectNum(1024, r) 107 | r.SetRatio(r1) 108 | testtools.CollectNum(1024, r) 109 | r.SetRatio(r2) 110 | testtools.CollectNum(1024, r) 111 | r.SetRatio(r3) 112 | testtools.CollectNum(1024, r) 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /speaker/speaker.go: -------------------------------------------------------------------------------- 1 | // Package speaker implements playback of beep.Streamer values through physical speakers. 2 | package speaker 3 | 4 | import ( 5 | "io" 6 | "sync" 7 | "time" 8 | 9 | "github.com/ebitengine/oto/v3" 10 | "github.com/pkg/errors" 11 | 12 | "github.com/gopxl/beep/v2" 13 | "github.com/gopxl/beep/v2/internal/util" 14 | ) 15 | 16 | const channelCount = 2 17 | const bitDepthInBytes = 2 18 | const bytesPerSample = bitDepthInBytes * channelCount 19 | const otoFormat = oto.FormatSignedInt16LE 20 | 21 | var ( 22 | mu sync.Mutex 23 | mixer beep.Mixer 24 | context *oto.Context 25 | player *oto.Player 26 | 27 | bufferDuration time.Duration 28 | ) 29 | 30 | // Init initializes audio playback through speaker. Must be called before using this package. 31 | // 32 | // The bufferSize argument specifies the number of samples of the speaker's buffer. Bigger 33 | // bufferSize means lower CPU usage and more reliable playback. Lower bufferSize means better 34 | // responsiveness and less delay. 35 | func Init(sampleRate beep.SampleRate, bufferSize int) error { 36 | if context != nil { 37 | return errors.New("speaker cannot be initialized more than once") 38 | } 39 | 40 | mixer = beep.Mixer{} 41 | 42 | // We split the total amount of buffer size between the driver and the player. 43 | // This seems to be a decent ratio on my machine, but it may have different 44 | // results on other OS's because of different underlying implementations. 45 | // Both buffers try to keep themselves filled, so the total buffered 46 | // number of samples should be some number less than bufferSize. 47 | driverBufferSize := bufferSize / 2 48 | playerBufferSize := bufferSize / 2 49 | 50 | var err error 51 | var readyChan chan struct{} 52 | context, readyChan, err = oto.NewContext(&oto.NewContextOptions{ 53 | SampleRate: int(sampleRate), 54 | ChannelCount: channelCount, 55 | Format: otoFormat, 56 | BufferSize: sampleRate.D(driverBufferSize), 57 | }) 58 | if err != nil { 59 | return errors.Wrap(err, "failed to initialize speaker") 60 | } 61 | <-readyChan 62 | 63 | player = context.NewPlayer(newReaderFromStreamer(&mixer)) 64 | player.SetBufferSize(playerBufferSize * bytesPerSample) 65 | player.Play() 66 | 67 | bufferDuration = sampleRate.D(bufferSize) 68 | 69 | return nil 70 | } 71 | 72 | // Close closes audio playback. However, the underlying driver context keeps existing, because 73 | // closing it isn't supported (https://github.com/hajimehoshi/oto/issues/149). In most cases, 74 | // there is certainly no need to call Close even when the program doesn't play anymore, because 75 | // in properly set systems, the default mixer handles multiple concurrent processes. 76 | func Close() { 77 | if player != nil { 78 | player.Close() 79 | player = nil 80 | Clear() 81 | } 82 | } 83 | 84 | // Lock locks the speaker. While locked, speaker won't pull new data from the playing Streamers. Lock 85 | // if you want to modify any currently playing Streamers to avoid race conditions. 86 | // 87 | // Always lock speaker for as little time as possible, to avoid playback glitches. 88 | func Lock() { 89 | mu.Lock() 90 | } 91 | 92 | // Unlock unlocks the speaker. Call after modifying any currently playing Streamer. 93 | func Unlock() { 94 | mu.Unlock() 95 | } 96 | 97 | // Play starts playing all provided Streamers through the speaker. 98 | func Play(s ...beep.Streamer) { 99 | mu.Lock() 100 | mixer.Add(s...) 101 | mu.Unlock() 102 | } 103 | 104 | // PlayAndWait plays all provided Streamers through the speaker and waits until they have all finished playing. 105 | func PlayAndWait(s ...beep.Streamer) { 106 | mu.Lock() 107 | var wg sync.WaitGroup 108 | wg.Add(len(s)) 109 | for _, e := range s { 110 | mixer.Add(beep.Seq(e, beep.Callback(func() { 111 | wg.Done() 112 | }))) 113 | } 114 | mu.Unlock() 115 | 116 | // Wait for the streamers to drain. 117 | wg.Wait() 118 | 119 | // Wait the expected time it takes for the samples to reach the driver. 120 | time.Sleep(bufferDuration) 121 | } 122 | 123 | // Suspend suspends the entire audio play. 124 | // 125 | // This function is intended to save resources when no audio is playing. 126 | // To suspend individual streams, use the beep.Ctrl. 127 | func Suspend() error { 128 | err := context.Suspend() 129 | if err != nil { 130 | return errors.Wrap(err, "failed to suspend the speaker") 131 | } 132 | return nil 133 | } 134 | 135 | // Resume resumes the entire audio play, which was suspended by Suspend. 136 | func Resume() error { 137 | err := context.Resume() 138 | if err != nil { 139 | return errors.Wrap(err, "failed to resume the speaker") 140 | } 141 | return nil 142 | } 143 | 144 | // Clear removes all currently playing Streamers from the speaker. 145 | // Previously buffered samples may still be played. 146 | func Clear() { 147 | mu.Lock() 148 | mixer.Clear() 149 | mu.Unlock() 150 | } 151 | 152 | // sampleReader is a wrapper for beep.Streamer to implement io.Reader. 153 | type sampleReader struct { 154 | s beep.Streamer 155 | buf [][2]float64 156 | } 157 | 158 | func newReaderFromStreamer(s beep.Streamer) *sampleReader { 159 | return &sampleReader{ 160 | s: s, 161 | } 162 | } 163 | 164 | // Read pulls samples from the streamer and fills buf with the encoded 165 | // samples. Read expects the size of buf be divisible by the length 166 | // of a sample (= channel count * bit depth in bytes). 167 | func (s *sampleReader) Read(buf []byte) (n int, err error) { 168 | // Read samples from streamer 169 | if len(buf)%bytesPerSample != 0 { 170 | return 0, errors.New("requested number of bytes do not align with the samples") 171 | } 172 | ns := len(buf) / bytesPerSample 173 | if len(s.buf) < ns { 174 | s.buf = make([][2]float64, ns) 175 | } 176 | ns, ok := s.stream(s.buf[:ns]) 177 | if !ok { 178 | if s.s.Err() != nil { 179 | return 0, errors.Wrap(s.s.Err(), "streamer returned error when requesting samples") 180 | } 181 | if ns == 0 { 182 | return 0, io.EOF 183 | } 184 | } 185 | 186 | // Convert samples to bytes 187 | for i := range s.buf[:ns] { 188 | for c := range s.buf[i] { 189 | val := s.buf[i][c] 190 | val = util.Clamp(val, -1, 1) 191 | valInt16 := int16(val * (1<<15 - 1)) 192 | low := byte(valInt16) 193 | high := byte(valInt16 >> 8) 194 | buf[i*bytesPerSample+c*bitDepthInBytes+0] = low 195 | buf[i*bytesPerSample+c*bitDepthInBytes+1] = high 196 | } 197 | } 198 | 199 | return ns * bytesPerSample, nil 200 | } 201 | 202 | // stream pull samples from the streamer while preventing concurrency 203 | // problems by locking the global mixer. 204 | func (s *sampleReader) stream(samples [][2]float64) (n int, ok bool) { 205 | mu.Lock() 206 | defer mu.Unlock() 207 | return s.s.Stream(samples) 208 | } 209 | -------------------------------------------------------------------------------- /speaker/speaker_test.go: -------------------------------------------------------------------------------- 1 | package speaker 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "testing" 7 | 8 | "github.com/gopxl/beep/v2/internal/testtools" 9 | ) 10 | 11 | func BenchmarkSampleReader_Read(b *testing.B) { 12 | // note: must be multiples of bytesPerSample 13 | bufferSizes := []int{64, 512, 8192, 32768} 14 | 15 | for _, bs := range bufferSizes { 16 | b.Run(fmt.Sprintf("with buffer size %d", bs), func(b *testing.B) { 17 | s, _ := testtools.RandomDataStreamer(b.N) 18 | r := newReaderFromStreamer(s) 19 | buf := make([]byte, bs) 20 | 21 | b.StartTimer() 22 | for { 23 | n, err := r.Read(buf) 24 | if err == io.EOF { 25 | break 26 | } 27 | if err != nil { 28 | panic(err) 29 | } 30 | if n != len(buf) { 31 | break 32 | } 33 | } 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /streamers.go: -------------------------------------------------------------------------------- 1 | package beep 2 | 3 | // Silence returns a Streamer which streams num samples of silence. If num is negative, silence is 4 | // streamed forever. 5 | // 6 | // Deprecated: beep.Silence has been moved to generators.Silence 7 | func Silence(num int) Streamer { 8 | return StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 9 | if num == 0 { 10 | return 0, false 11 | } 12 | if 0 < num && num < len(samples) { 13 | samples = samples[:num] 14 | } 15 | clear(samples) 16 | if num > 0 { 17 | num -= len(samples) 18 | } 19 | return len(samples), true 20 | }) 21 | } 22 | 23 | // Callback returns a Streamer, which does not stream any samples, but instead calls f the first 24 | // time its Stream method is called. The speaker is locked while f is called. 25 | func Callback(f func()) Streamer { 26 | return StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 27 | if f != nil { 28 | f() 29 | f = nil 30 | } 31 | return 0, false 32 | }) 33 | } 34 | 35 | // Iterate returns a Streamer which successively streams Streamers obtains by calling the provided g 36 | // function. The streaming stops when g returns nil. 37 | // 38 | // Iterate does not propagate errors from the generated Streamers. 39 | func Iterate(g func() Streamer) Streamer { 40 | var ( 41 | s Streamer 42 | first = true 43 | ) 44 | return StreamerFunc(func(samples [][2]float64) (n int, ok bool) { 45 | if first { 46 | s = g() 47 | first = false 48 | } 49 | if s == nil { 50 | return 0, false 51 | } 52 | for len(samples) > 0 { 53 | if s == nil { 54 | break 55 | } 56 | sn, sok := s.Stream(samples) 57 | if !sok { 58 | s = g() 59 | } 60 | samples = samples[sn:] 61 | n += sn 62 | } 63 | return n, true 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /vorbis/decode.go: -------------------------------------------------------------------------------- 1 | // Package vorbis implements audio data decoding in oggvorbis format. 2 | package vorbis 3 | 4 | import ( 5 | "io" 6 | 7 | "github.com/jfreymuth/oggvorbis" 8 | "github.com/pkg/errors" 9 | 10 | "github.com/gopxl/beep/v2" 11 | ) 12 | 13 | const ( 14 | govorbisPrecision = 2 15 | ) 16 | 17 | // Decode takes a ReadCloser containing audio data in ogg/vorbis format and returns a StreamSeekCloser, 18 | // which streams that audio. The Seek method will panic if rc is not io.Seeker. 19 | // 20 | // Do not close the supplied ReadSeekCloser, instead, use the Close method of the returned 21 | // StreamSeekCloser when you want to release the resources. 22 | func Decode(rc io.ReadCloser) (s beep.StreamSeekCloser, format beep.Format, err error) { 23 | defer func() { 24 | if err != nil { 25 | err = errors.Wrap(err, "ogg/vorbis") 26 | } 27 | }() 28 | d, err := oggvorbis.NewReader(rc) 29 | if err != nil { 30 | return nil, beep.Format{}, err 31 | } 32 | 33 | channels := d.Channels() 34 | if channels > 2 { 35 | channels = 2 36 | } 37 | 38 | format = beep.Format{ 39 | SampleRate: beep.SampleRate(d.SampleRate()), 40 | NumChannels: channels, 41 | Precision: govorbisPrecision, 42 | } 43 | 44 | return &decoder{rc, d, make([]float32, d.Channels()), nil}, format, nil 45 | } 46 | 47 | type decoder struct { 48 | closer io.Closer 49 | d *oggvorbis.Reader 50 | tmp []float32 51 | err error 52 | } 53 | 54 | func (d *decoder) Stream(samples [][2]float64) (n int, ok bool) { 55 | if d.err != nil { 56 | return 0, false 57 | } 58 | 59 | // https://xiph.org/vorbis/doc/vorbisfile/ov_read.html 60 | // https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810004.3.9 61 | var leftChannelIndex, rightChannelIndex int 62 | switch d.d.Channels() { 63 | case 0: 64 | d.err = errors.New("ogg/vorbis: invalid channel count: 0") 65 | return 0, false 66 | case 1: 67 | leftChannelIndex = 0 68 | rightChannelIndex = 0 69 | case 2: 70 | fallthrough 71 | case 4: 72 | leftChannelIndex = 0 73 | rightChannelIndex = 1 74 | case 3: 75 | fallthrough 76 | case 5: 77 | fallthrough 78 | case 6: 79 | fallthrough 80 | case 7: 81 | fallthrough 82 | case 8: 83 | fallthrough 84 | default: 85 | leftChannelIndex = 0 86 | rightChannelIndex = 2 87 | } 88 | 89 | for i := range samples { 90 | dn, err := d.d.Read(d.tmp) 91 | if dn == 0 { 92 | break 93 | } 94 | if dn < len(d.tmp) { 95 | d.err = errors.New("ogg/vorbis: could only read part of a frame") 96 | return 0, false 97 | } 98 | if err == io.EOF { 99 | return 0, false 100 | } 101 | if err != nil { 102 | d.err = errors.Wrap(err, "ogg/vorbis") 103 | return 0, false 104 | } 105 | 106 | samples[i][0] = float64(d.tmp[leftChannelIndex]) 107 | samples[i][1] = float64(d.tmp[rightChannelIndex]) 108 | n++ 109 | } 110 | return n, n > 0 111 | } 112 | 113 | func (d *decoder) Err() error { 114 | return d.err 115 | } 116 | 117 | func (d *decoder) Len() int { 118 | return int(d.d.Length()) 119 | } 120 | 121 | func (d *decoder) Position() int { 122 | return int(d.d.Position()) 123 | } 124 | 125 | func (d *decoder) Seek(p int) error { 126 | err := d.d.SetPosition(int64(p)) 127 | if err != nil { 128 | return errors.Wrap(err, "ogg/vorbis") 129 | } 130 | return nil 131 | } 132 | 133 | func (d *decoder) Close() error { 134 | err := d.closer.Close() 135 | if err != nil { 136 | return errors.Wrap(err, "ogg/vorbis") 137 | } 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /vorbis/decode_test.go: -------------------------------------------------------------------------------- 1 | package vorbis_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/gopxl/beep/v2/internal/testtools" 10 | "github.com/gopxl/beep/v2/vorbis" 11 | ) 12 | 13 | func TestDecoder_ReturnBehaviour(t *testing.T) { 14 | f, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples.ogg")) 15 | assert.NoError(t, err) 16 | defer f.Close() 17 | 18 | s, _, err := vorbis.Decode(f) 19 | assert.NoError(t, err) 20 | assert.Equal(t, 22050, s.Len()) 21 | 22 | testtools.AssertStreamerHasCorrectReturnBehaviour(t, s, s.Len()) 23 | } 24 | -------------------------------------------------------------------------------- /wav/decode_test.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gopxl/beep/v2" 9 | "github.com/gopxl/beep/v2/internal/testtools" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestDecode(t *testing.T) { 15 | wav := []byte{ 16 | // Riff mark 17 | 'R', 'I', 'F', 'F', 18 | // File size without riff mark and file size 19 | 0x38, 0x00, 0x00, 0x00, // 56 bytes 20 | // Wave mark 21 | 'W', 'A', 'V', 'E', 22 | 23 | // Fmt mark 24 | 'f', 'm', 't', ' ', 25 | // Format chunk size 26 | 0x10, 0x00, 0x00, 0x00, // 16 bytes 27 | // Format type 28 | 0x01, 0x00, // 1 = PCM 29 | // Number of channels, 30 | 0x02, 0x00, 31 | // Sample rate 32 | 0x44, 0xAC, 0x00, 0x00, // 44100 samples/sec 33 | // Byte rate 34 | 0x10, 0xB1, 0x02, 0x00, // 44100 * 2 bytes/sample precision * 2 channels = 176400 bytes/sec 35 | // Bytes per frame 36 | 0x04, 0x00, // 2 bytes/sample precision * 2 channels = 4 bytes/frame 37 | // Bits per sample 38 | 0x10, 0x00, // 2 bytes/sample precision = 16 bits/sample 39 | 40 | // Data mark 41 | 'd', 'a', 't', 'a', 42 | // Data size 43 | 0x14, 0x00, 0x00, 0x00, // 5 samples * 2 bytes/sample precision * 2 channels = 20 bytes 44 | // Data 45 | 0x00, 0x00, 0x00, 0x00, 46 | 0x00, 0x00, 0x00, 0x00, 47 | 0x00, 0x00, 0x00, 0x00, 48 | 0x00, 0x00, 0x00, 0x00, 49 | 0x00, 0x00, 0x00, 0x00, 50 | } 51 | 52 | r := bytes.NewReader(wav) 53 | 54 | s, f, err := Decode(r) 55 | if err != nil { 56 | t.Fatalf("failed to decode the WAV file: %v", err) 57 | } 58 | 59 | assert.Equal(t, beep.Format{ 60 | SampleRate: 44100, 61 | NumChannels: 2, 62 | Precision: 2, 63 | }, f) 64 | 65 | assert.NoError(t, s.Err()) 66 | assert.Equal(t, 5, s.Len()) 67 | assert.Equal(t, 0, s.Position()) 68 | 69 | samples := make([][2]float64, 3) 70 | // Stream first few bytes 71 | n, ok := s.Stream(samples) 72 | assert.Equal(t, 3, n) 73 | assert.Truef(t, ok, "the decoder failed to stream the samples") 74 | assert.Equal(t, 3, s.Position()) 75 | assert.NoError(t, s.Err()) 76 | // Drain the streamer 77 | n, ok = s.Stream(samples) 78 | assert.Equal(t, 2, n) 79 | assert.Truef(t, ok, "the decoder failed to stream the samples") 80 | assert.Equal(t, 5, s.Position()) 81 | assert.NoError(t, s.Err()) 82 | // Drain the streamer some more 83 | n, ok = s.Stream(samples) 84 | assert.Equal(t, 0, n) 85 | assert.Equal(t, 5, s.Position()) 86 | assert.Falsef(t, ok, "expected the decoder to return false after it was fully drained") 87 | assert.NoError(t, s.Err()) 88 | 89 | d, ok := s.(*decoder) 90 | if !ok { 91 | t.Fatal("Streamer is not a decoder") 92 | } 93 | 94 | assert.Equal(t, header{ 95 | RiffMark: [4]byte{'R', 'I', 'F', 'F'}, 96 | FileSize: 56, // without the riff mark and file size 97 | WaveMark: [4]byte{'W', 'A', 'V', 'E'}, 98 | FmtMark: [4]byte{'f', 'm', 't', ' '}, 99 | FormatSize: 16, 100 | FormatType: 1, // 1 = PCM 101 | NumChans: 2, 102 | SampleRate: 44100, 103 | ByteRate: 176400, // 44100 * 2 bytes/sample precision * 2 channels = 176400 bytes/sec 104 | BytesPerFrame: 4, // 2 bytes/sample precision * 2 channels = 4 bytes/frame 105 | BitsPerSample: 16, // 2 bytes/sample precision = 16 bits/sample 106 | DataMark: [4]byte{'d', 'a', 't', 'a'}, 107 | DataSize: 20, // 5 samples * 2 bytes/sample precision * 2 channels = 20 bytes 108 | }, d.h) 109 | } 110 | 111 | func TestDecoder_ReturnBehaviour(t *testing.T) { 112 | f, err := os.Open(testtools.TestFilePath("valid_44100hz_22050_samples.wav")) 113 | assert.NoError(t, err) 114 | defer f.Close() 115 | 116 | s, _, err := Decode(f) 117 | assert.NoError(t, err) 118 | assert.Equal(t, 22050, s.Len()) 119 | 120 | testtools.AssertStreamerHasCorrectReturnBehaviour(t, s, s.Len()) 121 | } 122 | -------------------------------------------------------------------------------- /wav/doc.go: -------------------------------------------------------------------------------- 1 | // Package wav implements audio data decoding and encoding in WAVE format. 2 | package wav 3 | -------------------------------------------------------------------------------- /wav/encode.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/pkg/errors" 10 | 11 | "github.com/gopxl/beep/v2" 12 | ) 13 | 14 | // Encode writes all audio streamed from s to w in WAVE format. 15 | // 16 | // Format precision must be 1 or 2 bytes. 17 | func Encode(w io.WriteSeeker, s beep.Streamer, format beep.Format) (err error) { 18 | defer func() { 19 | if err != nil { 20 | err = errors.Wrap(err, "wav") 21 | } 22 | }() 23 | 24 | if format.NumChannels <= 0 { 25 | return errors.New("wav: invalid number of channels (less than 1)") 26 | } 27 | if format.Precision != 1 && format.Precision != 2 && format.Precision != 3 { 28 | return errors.New("wav: unsupported precision, 1, 2 or 3 is supported") 29 | } 30 | 31 | h := header{ 32 | RiffMark: [4]byte{'R', 'I', 'F', 'F'}, 33 | FileSize: -1, // finalization 34 | WaveMark: [4]byte{'W', 'A', 'V', 'E'}, 35 | FmtMark: [4]byte{'f', 'm', 't', ' '}, 36 | FormatSize: 16, 37 | FormatType: 1, 38 | NumChans: int16(format.NumChannels), 39 | SampleRate: int32(format.SampleRate), 40 | ByteRate: int32(int(format.SampleRate) * format.NumChannels * format.Precision), 41 | BytesPerFrame: int16(format.NumChannels * format.Precision), 42 | BitsPerSample: int16(format.Precision) * 8, 43 | DataMark: [4]byte{'d', 'a', 't', 'a'}, 44 | DataSize: -1, // finalization 45 | } 46 | if err := binary.Write(w, binary.LittleEndian, &h); err != nil { 47 | return err 48 | } 49 | 50 | var ( 51 | bw = bufio.NewWriter(w) 52 | samples = make([][2]float64, 512) 53 | buffer = make([]byte, len(samples)*format.Width()) 54 | written int 55 | ) 56 | for { 57 | n, ok := s.Stream(samples) 58 | if !ok { 59 | break 60 | } 61 | buf := buffer 62 | switch { 63 | case format.Precision == 1: 64 | for _, sample := range samples[:n] { 65 | buf = buf[format.EncodeUnsigned(buf, sample):] 66 | } 67 | case format.Precision == 2 || format.Precision == 3: 68 | for _, sample := range samples[:n] { 69 | buf = buf[format.EncodeSigned(buf, sample):] 70 | } 71 | default: 72 | panic(fmt.Errorf("wav: encode: invalid precision: %d", format.Precision)) 73 | } 74 | nn, err := bw.Write(buffer[:n*format.Width()]) 75 | if err != nil { 76 | return err 77 | } 78 | written += nn 79 | } 80 | if err := bw.Flush(); err != nil { 81 | return err 82 | } 83 | 84 | // finalize header 85 | h.FileSize = int32(44 - 8 + written) // 44-8 is the size of the header without RIFF signature and the length of the RIFF chunk 86 | h.DataSize = int32(written) 87 | if _, err := w.Seek(0, io.SeekStart); err != nil { 88 | return err 89 | } 90 | if err := binary.Write(w, binary.LittleEndian, &h); err != nil { 91 | return err 92 | } 93 | if _, err := w.Seek(0, io.SeekEnd); err != nil { 94 | return err 95 | } 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /wav/encode_test.go: -------------------------------------------------------------------------------- 1 | package wav 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | 8 | "github.com/gopxl/beep/v2" 9 | "github.com/gopxl/beep/v2/effects" 10 | "github.com/gopxl/beep/v2/generators" 11 | "github.com/gopxl/beep/v2/internal/testtools" 12 | 13 | "github.com/orcaman/writerseeker" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func TestEncode(t *testing.T) { 18 | var f = beep.Format{ 19 | SampleRate: 44100, 20 | NumChannels: 2, 21 | Precision: 2, 22 | } 23 | var w writerseeker.WriterSeeker 24 | var s = generators.Silence(5) 25 | 26 | err := Encode(&w, s, f) 27 | if err != nil { 28 | t.Fatalf("encoding failed with error: %v", err) 29 | } 30 | 31 | r := w.BytesReader() 32 | expectedWrittenSize := 44 /* header length */ + 5*f.Precision*f.NumChannels /* number of samples * bytes per sample * number of channels */ 33 | assert.Equal(t, expectedWrittenSize, r.Len(), "the encoded file doesn't have the right size") 34 | 35 | encoded := make([]byte, r.Len()) 36 | _, err = w.Reader().Read(encoded) 37 | if err != nil { 38 | t.Fatalf("failed reading the buffer: %v", err) 39 | } 40 | 41 | // Everything is encoded using little endian. 42 | assert.Equal(t, []byte{ 43 | // Riff mark 44 | 'R', 'I', 'F', 'F', 45 | // File size without riff mark and file size 46 | 0x38, 0x00, 0x00, 0x00, // 56 bytes 47 | // Wave mark 48 | 'W', 'A', 'V', 'E', 49 | 50 | // Fmt mark 51 | 'f', 'm', 't', ' ', 52 | // Format chunk size 53 | 0x10, 0x00, 0x00, 0x00, // 16 bytes 54 | // Format type 55 | 0x01, 0x00, // 1 = PCM 56 | // Number of channels, 57 | 0x02, 0x00, 58 | // Sample rate 59 | 0x44, 0xAC, 0x00, 0x00, // 44100 samples/sec 60 | // Byte rate 61 | 0x10, 0xB1, 0x02, 0x00, // 44100 * 2 bytes/sample precision * 2 channels = 176400 bytes/sec 62 | // Bytes per frame 63 | 0x04, 0x00, // 2 bytes/sample precision * 2 channels = 4 bytes/frame 64 | // Bits per sample 65 | 0x10, 0x00, // 2 bytes/sample precision = 16 bits/sample 66 | 67 | // Data mark 68 | 'd', 'a', 't', 'a', 69 | // Data size 70 | 0x14, 0x00, 0x00, 0x00, // 5 samples * 2 bytes/sample precision * 2 channels = 20 bytes 71 | // Data 72 | 0x00, 0x00, 0x00, 0x00, 73 | 0x00, 0x00, 0x00, 0x00, 74 | 0x00, 0x00, 0x00, 0x00, 75 | 0x00, 0x00, 0x00, 0x00, 76 | 0x00, 0x00, 0x00, 0x00, 77 | }, encoded, "the encoded file isn't formatted as expected") 78 | } 79 | 80 | func TestEncodeDecodeRoundTrip(t *testing.T) { 81 | numChannelsS := []int{1, 2} 82 | precisions := []int{1, 2, 3} 83 | 84 | for _, numChannels := range numChannelsS { 85 | for _, precision := range precisions { 86 | name := fmt.Sprintf("%d_channel(s)_%d_precision", numChannels, precision) 87 | t.Run(name, func(t *testing.T) { 88 | var s beep.Streamer 89 | s, data := testtools.RandomDataStreamer(1000) 90 | 91 | if numChannels == 1 { 92 | s = effects.Mono(s) 93 | for i := range data { 94 | mix := (data[i][0] + data[i][1]) / 2 95 | data[i][0] = mix 96 | data[i][1] = mix 97 | } 98 | } 99 | 100 | var w writerseeker.WriterSeeker 101 | 102 | format := beep.Format{SampleRate: 44100, NumChannels: numChannels, Precision: precision} 103 | 104 | err := Encode(&w, s, format) 105 | assert.NoError(t, err) 106 | 107 | s, decodedFormat, err := Decode(w.Reader()) 108 | assert.NoError(t, err) 109 | assert.Equal(t, format, decodedFormat) 110 | 111 | actual := testtools.Collect(s) 112 | assert.Len(t, actual, 1000) 113 | 114 | // Delta is determined as follows: 115 | // The float values range from -1 to 1, which difference is 2.0. 116 | // For each byte of precision, there are 8 bits -> 2^(precision*8) different possible values. 117 | // So, fitting 2^(precision*8) values into a range of 2.0, each "step" must not 118 | // be bigger than 2.0 / math.Exp2(float64(precision*8)). 119 | delta := 2.0 / math.Exp2(float64(precision*8)) 120 | for i := range actual { 121 | // Adjust for clipping. 122 | if data[i][0] >= 1.0 { 123 | data[i][0] = 1.0 - 1.0/(math.Exp2(float64(precision)*8-1)) 124 | } 125 | if data[i][1] >= 1.0 { 126 | data[i][1] = 1.0 - 1.0/(math.Exp2(float64(precision)*8-1)) 127 | } 128 | 129 | if actual[i][0] <= data[i][0]-delta || actual[i][0] >= data[i][0]+delta { 130 | t.Fatalf("encoded & decoded sample doesn't match original. expected: %v, actual: %v", data[i][0], actual[i][0]) 131 | } 132 | if actual[i][1] <= data[i][1]-delta || actual[i][1] >= data[i][1]+delta { 133 | t.Fatalf("encoded & decoded sample doesn't match original. expected: %v, actual: %v", data[i][1], actual[i][1]) 134 | } 135 | } 136 | }) 137 | } 138 | } 139 | } 140 | --------------------------------------------------------------------------------