From fa772ddd77bdbb60c018ef99a7fa650122f63f1a Mon Sep 17 00:00:00 2001 From: Matias Lahti Date: Sat, 7 Nov 2020 19:22:59 +0200 Subject: [PATCH] feat(go/audio): implement basic audio output with oto splitting implementation into a separate package to potentially allow for other sorts of output, too. --- go.mod | 2 ++ go4k/audio/convertbuffer.go | 22 +++++++++++++ go4k/audio/convertbuffer_test.go | 48 ++++++++++++++++++++++++++++ go4k/audio/oto/otoplayer.go | 54 ++++++++++++++++++++++++++++++++ go4k/audio/player.go | 6 ++++ 5 files changed, 132 insertions(+) create mode 100644 go4k/audio/convertbuffer.go create mode 100644 go4k/audio/convertbuffer_test.go create mode 100644 go4k/audio/oto/otoplayer.go create mode 100644 go4k/audio/player.go diff --git a/go.mod b/go.mod index bb4f14c..acd2e12 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/vsariola/sointu go 1.15 + +require github.com/hajimehoshi/oto v0.6.6 diff --git a/go4k/audio/convertbuffer.go b/go4k/audio/convertbuffer.go new file mode 100644 index 0000000..e63f91e --- /dev/null +++ b/go4k/audio/convertbuffer.go @@ -0,0 +1,22 @@ +package audio + +import ( + "bytes" + "encoding/binary" + "fmt" + "math" +) + +// FloatBufferTo16BitLE is a naive helper method to convert []float32 buffers to +// 16-bit little-endian integer buffers. +// TODO: optimize/refactor this, current is far from the best solution +func FloatBufferTo16BitLE(buff []float32) ([]byte, error) { + var buf bytes.Buffer + for i, v := range buff { + uv := int16(v * math.MaxInt16) + if err := binary.Write(&buf, binary.LittleEndian, uv); err != nil { + return nil, fmt.Errorf("error converting buffer (@ %v, value %v) to bytes: %w", i, v, err) + } + } + return buf.Bytes(), nil +} diff --git a/go4k/audio/convertbuffer_test.go b/go4k/audio/convertbuffer_test.go new file mode 100644 index 0000000..77578a0 --- /dev/null +++ b/go4k/audio/convertbuffer_test.go @@ -0,0 +1,48 @@ +package audio + +import ( + "reflect" + "testing" +) + +func TestFloatBufferToBytes(t *testing.T) { + floats := []float32{0, 0.000489128, 0, 0.0019555532, 0, 0.0043964, 0, 0.007806882, 0, 0.012180306, 0, 0.017508084, 0, 0.023779746, 0, 0.030982954, 0, 0.039103523, 0, 0.04812544, 0, 0.05803088, 0, 0.068800256, 0, 0.08041221, 0, 0.09284368, 0, 0.10606992, 0, 0.120064534, 0, 0.13479951, 0, 0.1502453, 0, 0.16637078, 0, 0.18314338, 0, 0.20052913, 0, 0.21849263, 0, 0.23699719, 0, 0.2560048, 0, 0.27547634, 0, 0.29537144, 0, 0.31564865, 0, 0.33626547, 0, 0.35717854, 0, 0.37834346, 0, 0.39971504, 0, 0.4212474, 0, 0.4428938, 0, 0.46460703, 0, 0.48633927, 0, 0.50804216, 0, 0.52966696, 0, 0.5511646, 0, 0.57248586, 0, 0.5935812, 0, 0.6144009, 0, 0.63489544, 0, 0.6550152, 0, 0.67471063, 0, 0.6939326, 0, 0.712632, 0, 0.7307603, 0, 0.7482692, 0, 0.7651111, 0, 0.7812389} + bytes := []byte{0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x8f, 0x1, 0x0, 0x0, 0x3d, 0x2, 0x0, 0x0, 0xb, 0x3, 0x0, 0x0, 0xf7, 0x3, 0x0, 0x0, 0x1, 0x5, 0x0, 0x0, 0x28, 0x6, 0x0, 0x0, 0x6d, 0x7, 0x0, 0x0, 0xce, 0x8, 0x0, 0x0, 0x4a, 0xa, 0x0, 0x0, 0xe2, 0xb, 0x0, 0x0, 0x93, 0xd, 0x0, 0x0, 0x5e, 0xf, 0x0, 0x0, 0x40, 0x11, 0x0, 0x0, 0x3b, 0x13, 0x0, 0x0, 0x4b, 0x15, 0x0, 0x0, 0x71, 0x17, 0x0, 0x0, 0xaa, 0x19, 0x0, 0x0, 0xf7, 0x1b, 0x0, 0x0, 0x55, 0x1e, 0x0, 0x0, 0xc4, 0x20, 0x0, 0x0, 0x42, 0x23, 0x0, 0x0, 0xce, 0x25, 0x0, 0x0, 0x66, 0x28, 0x0, 0x0, 0xa, 0x2b, 0x0, 0x0, 0xb7, 0x2d, 0x0, 0x0, 0x6d, 0x30, 0x0, 0x0, 0x29, 0x33, 0x0, 0x0, 0xeb, 0x35, 0x0, 0x0, 0xb0, 0x38, 0x0, 0x0, 0x77, 0x3b, 0x0, 0x0, 0x3f, 0x3e, 0x0, 0x0, 0x7, 0x41, 0x0, 0x0, 0xcb, 0x43, 0x0, 0x0, 0x8c, 0x46, 0x0, 0x0, 0x46, 0x49, 0x0, 0x0, 0xf9, 0x4b, 0x0, 0x0, 0xa4, 0x4e, 0x0, 0x0, 0x43, 0x51, 0x0, 0x0, 0xd6, 0x53, 0x0, 0x0, 0x5c, 0x56, 0x0, 0x0, 0xd2, 0x58, 0x0, 0x0, 0x36, 0x5b, 0x0, 0x0, 0x88, 0x5d, 0x0, 0x0, 0xc6, 0x5f, 0x0, 0x0, 0xee, 0x61, 0x0, 0x0, 0xfe, 0x63} + converted, err := FloatBufferTo16BitLE(floats) + if err != nil { + t.Fatalf("FloatBufferTo16BitLE threw an error") + } + for i, v := range converted { + if bytes[i] != v { + t.Fail() + t.Errorf("Unexpected conversion output byte %x (expected %x) at position %v", v, bytes[i], i) + } + } + if !reflect.DeepEqual(converted, bytes) { + t.Fatalf("Unexpected conversion output from FloatBufferTo16BitLE") + } +} + +func TestFloatBufferToBytesLimits(t *testing.T) { + floats := []float32{0, 1, -1, 0.999, -0.999} + bytes := []byte{ + 0x0, 0x0, + 0xFF, 0x7F, // float 1 = 0x7FFF = 0111111111111111 + 0x01, 0x80, // float -1 = 0x8001 = 1000000000000001 + 0xDE, 0x7F, // float 0.999 = 0x7FDE = 0111111111011110 + 0x22, 0x80, // float -0.999 = 0x8022 = 1000000000100010 + } + converted, err := FloatBufferTo16BitLE(floats) + if err != nil { + t.Fatalf("FloatBufferTo16BitLE threw an error") + } + for i, v := range converted { + if bytes[i] != v { + t.Fail() + t.Errorf("Unexpected conversion output byte %x (expected %x) at position %v", v, bytes[i], i) + } + } + if !reflect.DeepEqual(converted, bytes) { + t.Fatalf("Unexpected conversion output from FloatBufferTo16BitLE") + } +} diff --git a/go4k/audio/oto/otoplayer.go b/go4k/audio/oto/otoplayer.go new file mode 100644 index 0000000..b2a2e76 --- /dev/null +++ b/go4k/audio/oto/otoplayer.go @@ -0,0 +1,54 @@ +package oto + +import ( + "fmt" + "github.com/hajimehoshi/oto" + "github.com/vsariola/sointu/go4k/audio" +) + +// OtoPlayer wraps github.com/hajimehoshi/oto to play sointu-style float32[] audio +type OtoPlayer struct { + context *oto.Context + player *oto.Player +} + +// Play implements the audio.Player interface for OtoPlayer +func (o *OtoPlayer) Play(floatBuffer []float32) (err error) { + if byteBuffer, err := audio.FloatBufferTo16BitLE(floatBuffer); err != nil { + return fmt.Errorf("error writing to player: %w", err) + } else if _, err := o.player.Write(byteBuffer); err != nil { + return fmt.Errorf("error writing to player: %w", err) + } else { + fmt.Printf("%#v\n", floatBuffer[0:100]) + fmt.Printf("%#v\n", byteBuffer[0:200]) + } + + return nil +} + +// Close disposes of resources +func (o *OtoPlayer) Close() error { + if err := o.player.Close(); err != nil { + return fmt.Errorf("error closing player: %w", err) + } + if err := o.context.Close(); err != nil { + return fmt.Errorf("error closing oto context: %w", err) + } + return nil +} + +const otoBufferSize = 8192 + +// NewPlayer creates and initializes a new OtoPlayer +func NewPlayer() (*OtoPlayer, error) { + context, err := oto.NewContext(44100, 2, 2, otoBufferSize) + if err != nil { + return nil, fmt.Errorf("cannot create oto context: %w", err) + } + player := context.NewPlayer() + + return &OtoPlayer{ + context: context, + player: player, + }, nil +} diff --git a/go4k/audio/player.go b/go4k/audio/player.go new file mode 100644 index 0000000..e5203ee --- /dev/null +++ b/go4k/audio/player.go @@ -0,0 +1,6 @@ +package audio + +type Player interface { + Play(buffer []float32) (err error) + Close() +}