From d129e982c7654044ef6a3d09104ed6b9ccf78b3b Mon Sep 17 00:00:00 2001 From: Lukas Herman Date: Thu, 1 Oct 2020 13:48:00 -0700 Subject: [PATCH] Add generic wave's Buffer --- pkg/wave/buffer.go | 145 ++++++++++++++++++++++++++++++++++++++++ pkg/wave/buffer_test.go | 71 ++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 pkg/wave/buffer.go create mode 100644 pkg/wave/buffer_test.go diff --git a/pkg/wave/buffer.go b/pkg/wave/buffer.go new file mode 100644 index 0000000..50cc42b --- /dev/null +++ b/pkg/wave/buffer.go @@ -0,0 +1,145 @@ +package wave + +import "fmt" + +var ( + errUnsupportedFormat = fmt.Errorf("Unsupported format") +) + +// Buffer is a buffer that can store any audio format. +type Buffer struct { + // TODO: Probably standardize the audio formats so that we don't need to have the following different types + // and duplicated codes for each type + bufferFloat32Interleaved []float32 + bufferFloat32NonInterleaved [][]float32 + bufferInt16Interleaved []int16 + bufferInt16NonInterleaved [][]int16 + tmp Audio +} + +// NewBuffer creates a new Buffer instance +func NewBuffer() *Buffer { + return &Buffer{} +} + +// Load loads the current owned Audio +func (buff *Buffer) Load() Audio { + return buff.tmp +} + +// StoreCopy makes a copy of src and store its copy. StoreCopy will reuse as much memory as it can +// from the previous copies. For example, if StoreCopy is given an audio that has the format from the previous call, +// StoreCopy will not allocate extra memory and only copy the content from src to the previous buffer. +func (buff *Buffer) StoreCopy(src Audio) { + switch src := src.(type) { + case *Float32Interleaved: + clone, ok := buff.tmp.(*Float32Interleaved) + if ok { + *clone = *src + } else { + copied := *src + clone = &copied + } + + neededSize := len(src.Data) + if len(buff.bufferFloat32Interleaved) < neededSize { + if cap(buff.bufferFloat32Interleaved) >= neededSize { + buff.bufferFloat32Interleaved = buff.bufferFloat32Interleaved[:neededSize] + } else { + buff.bufferFloat32Interleaved = make([]float32, neededSize) + } + } + + copy(buff.bufferFloat32Interleaved, src.Data) + buff.tmp = clone + + case *Float32NonInterleaved: + clone, ok := buff.tmp.(*Float32NonInterleaved) + if ok { + *clone = *src + } else { + copied := *src + clone = &copied + } + + neededSize := len(src.Data) + if len(buff.bufferFloat32NonInterleaved) < neededSize { + if cap(buff.bufferFloat32NonInterleaved) >= neededSize { + buff.bufferFloat32NonInterleaved = buff.bufferFloat32NonInterleaved[:neededSize] + } else { + buff.bufferFloat32NonInterleaved = make([][]float32, neededSize) + } + } + + for i := range src.Data { + neededSize := len(src.Data[i]) + if len(buff.bufferFloat32NonInterleaved[i]) < neededSize { + if cap(buff.bufferFloat32NonInterleaved[i]) >= neededSize { + buff.bufferFloat32NonInterleaved[i] = buff.bufferFloat32NonInterleaved[i][:neededSize] + } else { + buff.bufferFloat32NonInterleaved[i] = make([]float32, neededSize) + } + } + + copy(buff.bufferFloat32NonInterleaved[i], src.Data[i]) + } + buff.tmp = clone + + case *Int16Interleaved: + clone, ok := buff.tmp.(*Int16Interleaved) + if ok { + *clone = *src + } else { + copied := *src + clone = &copied + } + + neededSize := len(src.Data) + if len(buff.bufferInt16Interleaved) < neededSize { + if cap(buff.bufferInt16Interleaved) >= neededSize { + buff.bufferInt16Interleaved = buff.bufferInt16Interleaved[:neededSize] + } else { + buff.bufferInt16Interleaved = make([]int16, neededSize) + } + } + + copy(buff.bufferInt16Interleaved, src.Data) + buff.tmp = clone + + case *Int16NonInterleaved: + clone, ok := buff.tmp.(*Int16NonInterleaved) + if ok { + *clone = *src + } else { + copied := *src + clone = &copied + } + + neededSize := len(src.Data) + if len(buff.bufferInt16NonInterleaved) < neededSize { + if cap(buff.bufferInt16NonInterleaved) >= neededSize { + buff.bufferInt16NonInterleaved = buff.bufferInt16NonInterleaved[:neededSize] + } else { + buff.bufferInt16NonInterleaved = make([][]int16, neededSize) + } + } + + for i := range src.Data { + neededSize := len(src.Data[i]) + if len(buff.bufferInt16NonInterleaved[i]) < neededSize { + if cap(buff.bufferInt16NonInterleaved[i]) >= neededSize { + buff.bufferInt16NonInterleaved[i] = buff.bufferInt16NonInterleaved[i][:neededSize] + } else { + buff.bufferInt16NonInterleaved[i] = make([]int16, neededSize) + } + } + + copy(buff.bufferInt16NonInterleaved[i], src.Data[i]) + } + buff.tmp = clone + + default: + // TODO: Should have a routine to convert any format to one of the supported formats above + panic(errUnsupportedFormat) + } +} diff --git a/pkg/wave/buffer_test.go b/pkg/wave/buffer_test.go new file mode 100644 index 0000000..16d62ff --- /dev/null +++ b/pkg/wave/buffer_test.go @@ -0,0 +1,71 @@ +package wave + +import ( + "reflect" + "testing" +) + +func TestBufferStoreCopyAndLoad(t *testing.T) { + chunkInfo := ChunkInfo{ + Len: 4, + Channels: 2, + SamplingRate: 48000, + } + testCases := map[string]struct { + New func() EditableAudio + Update func(EditableAudio) + }{ + "Float32Interleaved": { + New: func() EditableAudio { + return NewFloat32Interleaved(chunkInfo) + }, + Update: func(src EditableAudio) { + src.Set(0, 0, Float32Sample(1)) + }, + }, + "Float32NonInterleaved": { + New: func() EditableAudio { + return NewFloat32NonInterleaved(chunkInfo) + }, + Update: func(src EditableAudio) { + src.Set(0, 0, Float32Sample(1)) + }, + }, + "Int16Interleaved": { + New: func() EditableAudio { + return NewInt16Interleaved(chunkInfo) + }, + Update: func(src EditableAudio) { + src.Set(1, 1, Int16Sample(2)) + }, + }, + "Int16NonInterleaved": { + New: func() EditableAudio { + return NewInt16NonInterleaved(chunkInfo) + }, + Update: func(src EditableAudio) { + src.Set(1, 1, Int16Sample(2)) + }, + }, + } + + buffer := NewBuffer() + + for name, testCase := range testCases { + // Since the test also wants to make sure that Copier can convert from 1 type to another, + // t.Run is not ideal since it'll run the tests separately + t.Log("Testing", name) + + src := testCase.New() + buffer.StoreCopy(src) + if !reflect.DeepEqual(buffer.Load(), src) { + t.Fatal("Expected the copied audio chunk to be identical with the source") + } + + testCase.Update(src) + buffer.StoreCopy(src) + if !reflect.DeepEqual(buffer.Load(), src) { + t.Fatal("Expected the copied audio chunk to be identical with the source after an update in source") + } + } +}