diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7d7c5cb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Added a changelog file ([#7](https://github.com/gopxl/beep/pull/7)) +- Support for single channel ogg/vorbis ([#10](https://github.com/gopxl/beep/pull/10)) + +### Fixed +- Fix FileSize for saving .wav ([#6](https://github.com/gopxl/beep/pull/6)) + +### Changed +- Upgrade Go version to 1.21 ([#2](https://github.com/gopxl/beep/pull/2)) +- Upgrade Oto version to 3.1 ([#3](https://github.com/gopxl/beep/pull/3)) + +## [v1.0.0] 2023-10-07 +- Forked [faiface/beep](https://github.com/faiface/beep) to [gopxl/beep](https://github.com/gopxl/beep). diff --git a/README.md b/README.md index e1476f6..72415ec 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Beep [![GoDoc](https://godoc.org/github.com/gopxl/beep?status.svg)](https://godoc.org/github.com/gopxl/beep) [![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/699679031603494954)](https://discord.gg/q2DK4MP) +# Beep [![GoDoc](https://godoc.org/github.com/gopxl/beep?status.svg)](https://godoc.org/github.com/gopxl/beep) [![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) A little package that brings sound to any Go application. Suitable for playback and audio-processing. diff --git a/ctrl.go b/ctrl.go index 88fd8bc..6dc7e30 100644 --- a/ctrl.go +++ b/ctrl.go @@ -4,25 +4,25 @@ package beep // // Wrap a Streamer in a Ctrl. // -// ctrl := &beep.Ctrl{Streamer: s} +// ctrl := &beep.Ctrl{Streamer: s} // // Then, we can pause the streaming (this will cause Ctrl to stream silence). // -// ctrl.Paused = true +// ctrl.Paused = true // // To completely stop a Ctrl before the wrapped Streamer is drained, just set the wrapped Streamer // to nil. // -// ctrl.Streamer = nil +// ctrl.Streamer = nil // // If you're playing a Streamer wrapped in a Ctrl through the speaker, you need to lock and unlock // the speaker when modifying the Ctrl to avoid race conditions. // -// speaker.Play(ctrl) -// // ... -// speaker.Lock() -// ctrl.Paused = true -// speaker.Unlock() +// speaker.Play(ctrl) +// // ... +// speaker.Lock() +// ctrl.Paused = true +// speaker.Unlock() type Ctrl struct { Streamer Streamer Paused bool diff --git a/go.mod b/go.mod index 44b4e87..5900e82 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,29 @@ module github.com/gopxl/beep -go 1.14 +go 1.21 require ( + github.com/ebitengine/oto/v3 v3.1.0 github.com/gdamore/tcell v1.3.0 github.com/hajimehoshi/go-mp3 v0.3.0 - github.com/hajimehoshi/oto v0.7.1 github.com/jfreymuth/oggvorbis v1.0.1 github.com/mewkiz/flac v1.0.7 + github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/ebitengine/purego v0.5.0 // indirect + github.com/gdamore/encoding v1.0.0 // indirect + github.com/icza/bitio v1.0.0 // indirect + github.com/jfreymuth/vorbis v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.0.2 // indirect + github.com/mattn/go-runewidth v0.0.4 // indirect + github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/text v0.3.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fcc03dc..03acfb3 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,12 @@ github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/oto/v3 v3.1.0 h1:9tChG6rizyeR2w3vsygTTTVVJ9QMMyu00m2yBOCch6U= +github.com/ebitengine/oto/v3 v3.1.0/go.mod h1:IK1QTnlfZK2GIB6ziyECm433hAdTaPpOsGMLhEyEGTg= +github.com/ebitengine/purego v0.5.0 h1:JrMGKfRIAM4/QVKaesIIT7m/UVjTj5GYhRSQYwfVdpo= +github.com/ebitengine/purego v0.5.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM= @@ -11,8 +17,6 @@ github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5 github.com/hajimehoshi/go-mp3 v0.3.0 h1:fTM5DXjp/DL2G74HHAs/aBGiS9Tg7wnp+jkU38bHy4g= github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= -github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk= -github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= github.com/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8= github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k= @@ -29,20 +33,28 @@ github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8= github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXiSoQu0r6RS1eA557AwJhlzHU= github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= +github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw= +github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/interface.go b/interface.go index 8e90919..8099767 100644 --- a/interface.go +++ b/interface.go @@ -86,13 +86,13 @@ type StreamSeekCloser interface { // // Example: // -// noise := StreamerFunc(func(samples [][2]float64) (n int, ok bool) { -// for i := range samples { -// samples[i][0] = rand.Float64()*2 - 1 -// samples[i][1] = rand.Float64()*2 - 1 -// } -// return len(samples), true -// }) +// noise := StreamerFunc(func(samples [][2]float64) (n int, ok bool) { +// for i := range samples { +// samples[i][0] = rand.Float64()*2 - 1 +// samples[i][1] = rand.Float64()*2 - 1 +// } +// return len(samples), true +// }) type StreamerFunc func(samples [][2]float64) (n int, ok bool) // Stream calls the wrapped streaming function. diff --git a/resample.go b/resample.go index 902a123..2082ac9 100644 --- a/resample.go +++ b/resample.go @@ -10,9 +10,9 @@ import "fmt" // Streamer which stream at a different sample rate will lead to a changed speed and pitch of the // playback. // -// sr := beep.SampleRate(48000) -// speaker.Init(sr, sr.N(time.Second/2)) -// speaker.Play(beep.Resample(3, format.SampleRate, sr, s)) +// sr := beep.SampleRate(48000) +// speaker.Init(sr, sr.N(time.Second/2)) +// speaker.Play(beep.Resample(3, format.SampleRate, sr, s)) // // In the example, the original sample rate of the source if format.SampleRate. We want to play it // at the speaker's native sample rate and thus we need to resample. @@ -21,12 +21,12 @@ import "fmt" // worse performance. Values below 1 or above 64 are invalid and Resample will panic. Here's a table // for deciding which quality to pick. // -// quality | use case -// --------|--------- -// 1 | very high performance, on-the-fly resampling, low quality -// 3-4 | good performance, on-the-fly resampling, good quality -// 6 | higher CPU usage, usually not suitable for on-the-fly resampling, very good quality -// >6 | even higher CPU usage, for offline resampling, very good quality +// quality | use case +// --------|--------- +// 1 | very high performance, on-the-fly resampling, low quality +// 3-4 | good performance, on-the-fly resampling, good quality +// 6 | higher CPU usage, usually not suitable for on-the-fly resampling, very good quality +// >6 | even higher CPU usage, for offline resampling, very good quality // // Sane quality values are usually below 16. Higher values will consume too much CPU, giving // negligible quality improvements. diff --git a/speaker/speaker.go b/speaker/speaker.go index a982d08..fc83208 100644 --- a/speaker/speaker.go +++ b/speaker/speaker.go @@ -2,21 +2,23 @@ package speaker import ( - "sync" - + "github.com/ebitengine/oto/v3" "github.com/gopxl/beep" - "github.com/hajimehoshi/oto" "github.com/pkg/errors" + "io" + "sync" ) +const channelCount = 2 +const bitDepthInBytes = 2 +const bytesPerSample = bitDepthInBytes * channelCount +const otoFormat = oto.FormatSignedInt16LE + var ( mu sync.Mutex mixer beep.Mixer - samples [][2]float64 - buf []byte context *oto.Context player *oto.Player - done chan struct{} ) // Init initializes audio playback through speaker. Must be called before using this package. @@ -25,53 +27,40 @@ var ( // bufferSize means lower CPU usage and more reliable playback. Lower bufferSize means better // responsiveness and less delay. func Init(sampleRate beep.SampleRate, bufferSize int) error { - mu.Lock() - defer mu.Unlock() - - Close() + if context != nil { + return errors.New("speaker cannot be initialized more than once") + } mixer = beep.Mixer{} - numBytes := bufferSize * 4 - samples = make([][2]float64, bufferSize) - buf = make([]byte, numBytes) - var err error - context, err = oto.NewContext(int(sampleRate), 2, 2, numBytes) + var readyChan chan struct{} + context, readyChan, err := oto.NewContext(&oto.NewContextOptions{ + SampleRate: int(sampleRate), + ChannelCount: channelCount, + Format: otoFormat, + BufferSize: sampleRate.D(bufferSize), + }) if err != nil { return errors.Wrap(err, "failed to initialize speaker") } - player = context.NewPlayer() - - done = make(chan struct{}) + <-readyChan - go func() { - for { - select { - default: - update() - case <-done: - return - } - } - }() + player = context.NewPlayer(newReaderFromStreamer(&mixer)) + player.Play() return nil } -// Close closes the playback and the driver. In most cases, there is certainly no need to call Close -// even when the program doesn't play anymore, because in properly set systems, the default mixer -// handles multiple concurrent processes. It's only when the default device is not a virtual but hardware -// device, that you'll probably want to manually manage the device from your application. +// Close closes audio playback. However, the underlying driver context keeps existing, because +// closing it isn't supported (https://github.com/hajimehoshi/oto/issues/149). In most cases, +// there is certainly no need to call Close even when the program doesn't play anymore, because +// in properly set systems, the default mixer handles multiple concurrent processes. func Close() { if player != nil { - if done != nil { - done <- struct{}{} - done = nil - } player.Close() - context.Close() player = nil + Clear() } } @@ -96,22 +85,51 @@ func Play(s ...beep.Streamer) { } // Clear removes all currently playing Streamers from the speaker. +// Previously buffered samples may still be played. func Clear() { mu.Lock() mixer.Clear() mu.Unlock() } -// update pulls new data from the playing Streamers and sends it to the speaker. Blocks until the -// data is sent and started playing. -func update() { - mu.Lock() - mixer.Stream(samples) - mu.Unlock() +// sampleReader is a wrapper for beep.Streamer to implement io.Reader. +type sampleReader struct { + s beep.Streamer + buf [][2]float64 +} + +func newReaderFromStreamer(s beep.Streamer) *sampleReader { + return &sampleReader{ + s: s, + } +} + +// Read pulls samples from the streamer and fills buf with the encoded +// samples. Read expects the size of buf be divisible by the length +// of a sample (= channel count * bit depth in bytes). +func (s *sampleReader) Read(buf []byte) (n int, err error) { + // Read samples from streamer + if len(buf)%bytesPerSample != 0 { + return 0, errors.New("requested number of bytes do not align with the samples") + } + ns := len(buf) / bytesPerSample + if len(s.buf) < ns { + s.buf = make([][2]float64, ns) + } + ns, ok := s.stream(s.buf[:ns]) + if !ok { + if s.s.Err() != nil { + return 0, errors.Wrap(s.s.Err(), "streamer returned error when requesting samples") + } + if ns == 0 { + return 0, io.EOF + } + } - for i := range samples { - for c := range samples[i] { - val := samples[i][c] + // Convert samples to bytes + for i := range s.buf[:ns] { + for c := range s.buf[i] { + val := s.buf[i][c] if val < -1 { val = -1 } @@ -121,10 +139,18 @@ func update() { valInt16 := int16(val * (1<<15 - 1)) low := byte(valInt16) high := byte(valInt16 >> 8) - buf[i*4+c*2+0] = low - buf[i*4+c*2+1] = high + buf[i*bytesPerSample+c*bitDepthInBytes+0] = low + buf[i*bytesPerSample+c*bitDepthInBytes+1] = high } } - player.Write(buf) + return ns * bytesPerSample, nil +} + +// stream pull samples from the streamer while preventing concurrency +// problems by locking the global mixer. +func (s *sampleReader) stream(samples [][2]float64) (n int, ok bool) { + mu.Lock() + defer mu.Unlock() + return s.s.Stream(samples) } diff --git a/wav/decode_test.go b/wav/decode_test.go new file mode 100644 index 0000000..d85640f --- /dev/null +++ b/wav/decode_test.go @@ -0,0 +1,105 @@ +package wav + +import ( + "bytes" + "github.com/gopxl/beep" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDecode(t *testing.T) { + wav := []byte{ + // Riff mark + 'R', 'I', 'F', 'F', + // File size without riff mark and file size + 0x38, 0x00, 0x00, 0x00, // 56 bytes + // Wave mark + 'W', 'A', 'V', 'E', + + // Fmt mark + 'f', 'm', 't', ' ', + // Format chunk size + 0x10, 0x00, 0x00, 0x00, // 16 bytes + // Format type + 0x01, 0x00, // 1 = PCM + // Number of channels, + 0x02, 0x00, + // Sample rate + 0x44, 0xAC, 0x00, 0x00, // 44100 samples/sec + // Byte rate + 0x10, 0xB1, 0x02, 0x00, // 44100 * 2 bytes/sample precision * 2 channels = 176400 bytes/sec + // Bytes per frame + 0x04, 0x00, // 2 bytes/sample precision * 2 channels = 4 bytes/frame + // Bits per sample + 0x10, 0x00, // 2 bytes/sample precision = 16 bits/sample + + // Data mark + 'd', 'a', 't', 'a', + // Data size + 0x14, 0x00, 0x00, 0x00, // 5 samples * 2 bytes/sample precision * 2 channels = 20 bytes + // Data + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + } + + r := bytes.NewReader(wav) + + s, f, err := Decode(r) + if err != nil { + t.Fatalf("failed to decode the WAV file: %v", err) + } + + assert.Equal(t, beep.Format{ + SampleRate: 44100, + NumChannels: 2, + Precision: 2, + }, f) + + assert.NoError(t, s.Err()) + assert.Equal(t, 5, s.Len()) + assert.Equal(t, 0, s.Position()) + + samples := make([][2]float64, 3) + // Stream first few bytes + n, ok := s.Stream(samples) + assert.Equal(t, 3, n) + assert.Truef(t, ok, "the decoder failed to stream the samples") + assert.Equal(t, 3, s.Position()) + assert.NoError(t, s.Err()) + // Drain the streamer + n, ok = s.Stream(samples) + assert.Equal(t, 2, n) + assert.Truef(t, ok, "the decoder failed to stream the samples") + assert.Equal(t, 5, s.Position()) + assert.NoError(t, s.Err()) + // Drain the streamer some more + n, ok = s.Stream(samples) + assert.Equal(t, 0, n) + assert.Equal(t, 5, s.Position()) + assert.Falsef(t, ok, "expected the decoder to return false after it was fully drained") + assert.NoError(t, s.Err()) + + d, ok := s.(*decoder) + if !ok { + t.Fatal("Streamer is not a decoder") + } + + assert.Equal(t, header{ + RiffMark: [4]byte{'R', 'I', 'F', 'F'}, + FileSize: 56, // without the riff mark and file size + WaveMark: [4]byte{'W', 'A', 'V', 'E'}, + FmtMark: [4]byte{'f', 'm', 't', ' '}, + FormatSize: 16, + FormatType: 1, // 1 = PCM + NumChans: 2, + SampleRate: 44100, + ByteRate: 176400, // 44100 * 2 bytes/sample precision * 2 channels = 176400 bytes/sec + BytesPerFrame: 4, // 2 bytes/sample precision * 2 channels = 4 bytes/frame + BitsPerSample: 16, // 2 bytes/sample precision = 16 bits/sample + DataMark: [4]byte{'d', 'a', 't', 'a'}, + DataSize: 20, // 5 samples * 2 bytes/sample precision * 2 channels = 20 bytes + }, d.h) +} diff --git a/wav/encode.go b/wav/encode.go index 16ff420..b9de7d7 100644 --- a/wav/encode.go +++ b/wav/encode.go @@ -81,7 +81,7 @@ func Encode(w io.WriteSeeker, s beep.Streamer, format beep.Format) (err error) { } // finalize header - h.FileSize = int32(44 + written) // 44 is the size of the header + h.FileSize = int32(44 - 8 + written) // 44-8 is the size of the header without RIFF signature and the length of the RIFF chunk h.DataSize = int32(written) if _, err := w.Seek(0, io.SeekStart); err != nil { return err diff --git a/wav/encode_test.go b/wav/encode_test.go new file mode 100644 index 0000000..af62219 --- /dev/null +++ b/wav/encode_test.go @@ -0,0 +1,71 @@ +package wav + +import ( + "github.com/gopxl/beep" + "github.com/orcaman/writerseeker" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestEncode(t *testing.T) { + var f = beep.Format{ + SampleRate: 44100, + NumChannels: 2, + Precision: 2, + } + var w writerseeker.WriterSeeker + var s = beep.Silence(5) + + err := Encode(&w, s, f) + if err != nil { + t.Fatalf("encoding failed with error: %v", err) + } + + r := w.BytesReader() + expectedWrittenSize := 44 /* header length */ + 5*f.Precision*f.NumChannels /* number of samples * bytes per sample * number of channels */ + assert.Equal(t, expectedWrittenSize, r.Len(), "the encoded file doesn't have the right size") + + encoded := make([]byte, r.Len()) + _, err = w.Reader().Read(encoded) + if err != nil { + t.Fatalf("failed reading the buffer: %v", err) + } + + // Everything is encoded using little endian. + assert.Equal(t, []byte{ + // Riff mark + 'R', 'I', 'F', 'F', + // File size without riff mark and file size + 0x38, 0x00, 0x00, 0x00, // 56 bytes + // Wave mark + 'W', 'A', 'V', 'E', + + // Fmt mark + 'f', 'm', 't', ' ', + // Format chunk size + 0x10, 0x00, 0x00, 0x00, // 16 bytes + // Format type + 0x01, 0x00, // 1 = PCM + // Number of channels, + 0x02, 0x00, + // Sample rate + 0x44, 0xAC, 0x00, 0x00, // 44100 samples/sec + // Byte rate + 0x10, 0xB1, 0x02, 0x00, // 44100 * 2 bytes/sample precision * 2 channels = 176400 bytes/sec + // Bytes per frame + 0x04, 0x00, // 2 bytes/sample precision * 2 channels = 4 bytes/frame + // Bits per sample + 0x10, 0x00, // 2 bytes/sample precision = 16 bits/sample + + // Data mark + 'd', 'a', 't', 'a', + // Data size + 0x14, 0x00, 0x00, 0x00, // 5 samples * 2 bytes/sample precision * 2 channels = 20 bytes + // Data + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + }, encoded, "the encoded file isn't formatted as expected") +}