Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into mixer-v2
Browse files Browse the repository at this point in the history
# Conflicts:
#	compositors.go
  • Loading branch information
MarkKremer committed Sep 11, 2024
2 parents 2858cbe + 376f6c5 commit dd1b9b7
Show file tree
Hide file tree
Showing 70 changed files with 1,617 additions and 379 deletions.
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: "weekly"
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# Beep

[![GoDoc](https://godoc.org/github.com/gopxl/beep?status.svg)](https://godoc.org/github.com/gopxl/beep)
[![Go Reference](https://pkg.go.dev/badge/github.com/gopxl/beep/v2.svg)](https://pkg.go.dev/github.com/gopxl/beep/v2)
[![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)
[![Coverage Status](https://coveralls.io/repos/github/gopxl/beep/badge.svg?branch=main)](https://coveralls.io/github/gopxl/beep?branch=main)
[![Go Report Card](https://goreportcard.com/badge/github.com/gopxl/beep)](https://goreportcard.com/report/github.com/gopxl/beep)
[![Discord Chat](https://img.shields.io/discord/1158461233121468496)](https://discord.gg/erpa32cB)
[![Go Report Card](https://goreportcard.com/badge/github.com/gopxl/beep/v2)](https://goreportcard.com/report/github.com/gopxl/beep/v2)
[![Discord Chat](https://img.shields.io/discord/1158461233121468496)](https://discord.gg/hPBTTXGDU3)


A little package that brings sound to any Go application. Suitable for playback and audio-processing.

```
go get -u github.com/gopxl/beep
go get -u github.com/gopxl/beep/v2
```

## Features

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.

- **Decode and play WAV, MP3, OGG, and FLAC.**
- **Decode and play WAV, MP3, OGG, FLAC and MIDI.**
- **Encode and save WAV.**
- **Very simple API.** Limiting the support to stereo (two channel) audio made it possible to simplify the architecture and the API.
- **Rich library of compositors and effects.** Loop, pause/resume, change volume, mix, sequence, change playback speed, and more.
Expand Down Expand Up @@ -55,5 +55,5 @@ Running an already built application should work with no extra dependencies.
- [Microphone support for Beep (a wrapper around PortAudio)](https://github.com/MarkKremer/microphone)

## Projects using Beep

- [retro](https://github.com/Malwarize/retro)
- [Mifasol music server](https://github.com/jypelle/mifasol)
28 changes: 10 additions & 18 deletions buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"math"
"time"

"github.com/gopxl/beep/v2/internal/util"
)

// SampleRate is the number of samples per second.
Expand Down Expand Up @@ -63,11 +65,11 @@ func (f Format) DecodeUnsigned(p []byte) (sample [2]float64, n int) {
func (f Format) encode(signed bool, p []byte, sample [2]float64) (n int) {
switch {
case f.NumChannels == 1:
x := norm((sample[0] + sample[1]) / 2)
x := util.Clamp((sample[0]+sample[1])/2, -1, 1)
p = p[encodeFloat(signed, f.Precision, p, x):]
case f.NumChannels >= 2:
for c := range sample {
x := norm(sample[c])
x := util.Clamp(sample[c], -1, 1)
p = p[encodeFloat(signed, f.Precision, p, x):]
}
for c := len(sample); c < f.NumChannels; c++ {
Expand Down Expand Up @@ -128,36 +130,26 @@ func decodeFloat(signed bool, precision int, p []byte) (x float64, n int) {

func floatToSigned(precision int, x float64) uint64 {
if x < 0 {
compl := uint64(-x * (math.Exp2(float64(precision)*8-1) - 1))
compl := uint64(-x * math.Exp2(float64(precision)*8-1))
return uint64(1<<uint(precision*8)) - compl
}
return uint64(x * (math.Exp2(float64(precision)*8-1) - 1))
return uint64(math.Min(x*math.Exp2(float64(precision)*8-1), math.Exp2(float64(precision)*8-1)-1))
}

func floatToUnsigned(precision int, x float64) uint64 {
return uint64((x + 1) / 2 * (math.Exp2(float64(precision)*8) - 1))
return uint64(math.Min((x+1)/2*math.Exp2(float64(precision)*8), math.Exp2(float64(precision)*8)-1))
}

func signedToFloat(precision int, xUint64 uint64) float64 {
if xUint64 >= 1<<uint(precision*8-1) {
compl := 1<<uint(precision*8) - xUint64
return -float64(int64(compl)) / (math.Exp2(float64(precision)*8-1) - 1)
return -float64(int64(compl)) / math.Exp2(float64(precision)*8-1)
}
return float64(int64(xUint64)) / (math.Exp2(float64(precision)*8-1) - 1)
return float64(int64(xUint64)) / math.Exp2(float64(precision)*8-1)
}

func unsignedToFloat(precision int, xUint64 uint64) float64 {
return float64(xUint64)/(math.Exp2(float64(precision)*8)-1)*2 - 1
}

func norm(x float64) float64 {
if x < -1 {
return -1
}
if x > +1 {
return +1
}
return x
return float64(xUint64)/(math.Exp2(float64(precision)*8))*2 - 1
}

// Buffer is a storage for audio data. You can think of it as a bytes.Buffer for audio samples.
Expand Down
155 changes: 153 additions & 2 deletions buffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,161 @@ import (
"math/rand"
"testing"

"github.com/gopxl/beep"
"github.com/gopxl/beep/generators"
"github.com/stretchr/testify/assert"

"github.com/gopxl/beep/v2"
"github.com/gopxl/beep/v2/generators"
)

type bufferFormatTestCase struct {
Name string
Precision int
NumChannels int
Signed bool
Bytes []byte
Samples [2]float64
SkipDecodeTest bool
}

var bufferFormatTests = []bufferFormatTestCase{
// See https://gist.github.com/endolith/e8597a58bcd11a6462f33fa8eb75c43d
// for an explanation about the asymmetry in sample encodings in WAV when
// converting between ints and floats. Note that Beep does not follow the
// suggested solution. Instead, integer samples are divided by 1 more, so
// that the resulting float value falls within the range of -1.0 and 1.0.
// This is similar to how some other tools do the conversion.
{
Name: "1 channel 8bit WAV negative full scale",
Precision: 1,
NumChannels: 1,
Signed: false,
Bytes: []byte{0x00},
Samples: [2]float64{-1.0, -1.0},
},
{
Name: "1 channel 8bit WAV midpoint",
Precision: 1,
NumChannels: 1,
Signed: false,
Bytes: []byte{0x80},
Samples: [2]float64{0.0, 0.0},
},
{
// Because the WAV integer range is asymmetric, converting it to float
// by division will not result in an exactly 1.0 full scale float value.
// It will be 1 least significant bit integer value lower. "1", converted
// to float for an 8-bit WAV sample is 1 / (1 << 7).
Name: "1 channel 8bit WAV positive full scale minus 1 significant bit",
Precision: 1,
NumChannels: 1,
Signed: false,
Bytes: []byte{0xFF},
Samples: [2]float64{1.0 - (1.0 / (1 << 7)), 1.0 - (1.0 / (1 << 7))},
},
{
Name: "2 channel 8bit WAV full scale",
Precision: 1,
NumChannels: 2,
Signed: false,
Bytes: []byte{0x00, 0xFF},
Samples: [2]float64{-1.0, 1.0 - (1.0 / (1 << 7))},
},
{
Name: "1 channel 16bit WAV negative full scale",
Precision: 2,
NumChannels: 1,
Signed: true,
Bytes: []byte{0x00, 0x80},
Samples: [2]float64{-1.0, -1.0},
},
{
Name: "1 channel 16bit WAV midpoint",
Precision: 2,
NumChannels: 1,
Signed: true,
Bytes: []byte{0x00, 0x00},
Samples: [2]float64{0.0, 0.0},
},
{
// Because the WAV integer range is asymmetric, converting it to float
// by division will not result in an exactly 1.0 full scale float value.
// It will be 1 least significant bit integer value lower. "1", converted
// to float for an 16-bit WAV sample is 1 / (1 << 15).
Name: "1 channel 16bit WAV positive full scale minus 1 significant bit",
Precision: 2,
NumChannels: 1,
Signed: true,
Bytes: []byte{0xFF, 0x7F},
Samples: [2]float64{1.0 - (1.0 / (1 << 15)), 1.0 - (1.0 / (1 << 15))},
},
{
Name: "1 channel 8bit WAV float positive full scale clipping test",
Precision: 1,
NumChannels: 1,
Signed: false,
Bytes: []byte{0xFF},
Samples: [2]float64{1.0, 1.0},
SkipDecodeTest: true,
},
{
Name: "1 channel 16bit WAV float positive full scale clipping test",
Precision: 2,
NumChannels: 1,
Signed: true,
Bytes: []byte{0xFF, 0x7F},
Samples: [2]float64{1.0, 1.0},
SkipDecodeTest: true,
},
}

func TestFormatDecode(t *testing.T) {
for _, test := range bufferFormatTests {
if test.SkipDecodeTest {
continue
}

t.Run(test.Name, func(t *testing.T) {
format := beep.Format{
SampleRate: 44100,
Precision: test.Precision,
NumChannels: test.NumChannels,
}

var sample [2]float64
var n int
if test.Signed {
sample, n = format.DecodeSigned(test.Bytes)
} else {
sample, n = format.DecodeUnsigned(test.Bytes)
}
assert.Equal(t, len(test.Bytes), n)
assert.Equal(t, test.Samples, sample)
})
}
}

func TestFormatEncode(t *testing.T) {
for _, test := range bufferFormatTests {
t.Run(test.Name, func(t *testing.T) {
format := beep.Format{
SampleRate: 44100,
Precision: test.Precision,
NumChannels: test.NumChannels,
}

bytes := make([]byte, test.Precision*test.NumChannels)
var n int
if test.Signed {
n = format.EncodeSigned(bytes, test.Samples)
} else {
n = format.EncodeUnsigned(bytes, test.Samples)
}
assert.Equal(t, len(test.Bytes), n)
assert.Equal(t, test.Bytes, bytes)
})
}
}

func TestFormatEncodeDecode(t *testing.T) {
formats := make(chan beep.Format)
go func() {
Expand Down
Loading

0 comments on commit dd1b9b7

Please sign in to comment.