mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-19 13:34:34 -04:00
feat: upgrade oto and output float audio
This commit is contained in:
parent
890ebe3294
commit
81a6d1acea
@ -1,32 +0,0 @@
|
||||
package oto
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
// FloatBufferTo16BitLE is a naive helper method to convert []float32 buffers to
|
||||
// 16-bit little-endian, but encoded in byte buffer
|
||||
//
|
||||
// Appends the encoded bytes into "to" slice, allowing you to preallocate the
|
||||
// capacity or just use nil
|
||||
func FloatBufferTo16BitLE(from sointu.AudioBuffer, to []byte) []byte {
|
||||
for _, v := range from {
|
||||
left := to16BitSample(v[0])
|
||||
right := to16BitSample(v[1])
|
||||
to = append(to, byte(left&255), byte(left>>8), byte(right&255), byte(right>>8))
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
// convert float32 to int16, clamping to min and max
|
||||
func to16BitSample(v float32) int16 {
|
||||
if v < -1.0 {
|
||||
return -math.MaxInt16
|
||||
}
|
||||
if v > 1.0 {
|
||||
return math.MaxInt16
|
||||
}
|
||||
return int16(v * math.MaxInt16)
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package oto_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/oto"
|
||||
)
|
||||
|
||||
func TestFloatBufferToBytes(t *testing.T) {
|
||||
floats := sointu.AudioBuffer{{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 := oto.FloatBufferTo16BitLE(floats, nil)
|
||||
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 := sointu.AudioBuffer{{0, 1}, {-1, 0.999}, {-0.999, 0}}
|
||||
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
|
||||
0x0, 0x0,
|
||||
}
|
||||
converted := oto.FloatBufferTo16BitLE(floats, nil)
|
||||
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")
|
||||
}
|
||||
}
|
108
oto/oto.go
108
oto/oto.go
@ -1,55 +1,99 @@
|
||||
package oto
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"github.com/hajimehoshi/oto"
|
||||
"github.com/ebitengine/oto/v3"
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
type OtoContext oto.Context
|
||||
type OtoOutput struct {
|
||||
player *oto.Player
|
||||
tmpBuffer []byte
|
||||
}
|
||||
const latency = 2048 // in samples at 44100 Hz = ~46 ms
|
||||
|
||||
func (c *OtoContext) Output() sointu.AudioOutput {
|
||||
return &OtoOutput{player: (*oto.Context)(c).NewPlayer(), tmpBuffer: make([]byte, 0)}
|
||||
}
|
||||
type (
|
||||
OtoContext oto.Context
|
||||
|
||||
const otoBufferSize = 8192
|
||||
OtoPlayer struct {
|
||||
player *oto.Player
|
||||
reader *OtoReader
|
||||
}
|
||||
|
||||
OtoReader struct {
|
||||
audioSource sointu.AudioSource
|
||||
tmpBuffer sointu.AudioBuffer
|
||||
waitGroup sync.WaitGroup
|
||||
err error
|
||||
errMutex sync.RWMutex
|
||||
}
|
||||
)
|
||||
|
||||
// NewPlayer creates and initializes a new OtoPlayer
|
||||
func NewContext() (*OtoContext, error) {
|
||||
context, err := oto.NewContext(44100, 2, 2, otoBufferSize)
|
||||
op := oto.NewContextOptions{}
|
||||
op.SampleRate = 44100
|
||||
op.ChannelCount = 2
|
||||
op.Format = oto.FormatFloat32LE
|
||||
context, readyChan, err := oto.NewContext(&op)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create oto context: %w", err)
|
||||
}
|
||||
<-readyChan
|
||||
return (*OtoContext)(context), nil
|
||||
}
|
||||
|
||||
func (c *OtoContext) Close() error {
|
||||
if err := (*oto.Context)(c).Close(); err != nil {
|
||||
return fmt.Errorf("cannot close oto context: %w", err)
|
||||
}
|
||||
return nil
|
||||
func (c *OtoContext) Play(r sointu.AudioSource) sointu.CloserWaiter {
|
||||
reader := &OtoReader{audioSource: r}
|
||||
reader.waitGroup.Add(1)
|
||||
player := (*oto.Context)(c).NewPlayer(reader)
|
||||
player.SetBufferSize(latency * 8)
|
||||
player.Play()
|
||||
return OtoPlayer{player: player, reader: reader}
|
||||
}
|
||||
|
||||
// Play implements the audio.Player interface for OtoPlayer
|
||||
func (o *OtoOutput) WriteAudio(floatBuffer sointu.AudioBuffer) (err error) {
|
||||
// we reuse the old capacity tmpBuffer by setting its length to zero. then,
|
||||
// we save the tmpBuffer so we can reuse it next time
|
||||
o.tmpBuffer = FloatBufferTo16BitLE(floatBuffer, o.tmpBuffer[:0])
|
||||
if _, err := o.player.Write(o.tmpBuffer); err != nil {
|
||||
return fmt.Errorf("cannot write to player: %w", err)
|
||||
}
|
||||
return nil
|
||||
func (o OtoPlayer) Wait() {
|
||||
o.reader.waitGroup.Wait()
|
||||
}
|
||||
|
||||
// Close disposes of resources
|
||||
func (o *OtoOutput) Close() error {
|
||||
if err := o.player.Close(); err != nil {
|
||||
return fmt.Errorf("cannot close oto player: %w", err)
|
||||
}
|
||||
return nil
|
||||
func (o OtoPlayer) Close() error {
|
||||
o.reader.closeWithError(errors.New("OtoPlayer was closed"))
|
||||
return o.player.Close()
|
||||
}
|
||||
|
||||
func (o *OtoReader) Read(b []byte) (n int, err error) {
|
||||
o.errMutex.RLock()
|
||||
if o.err != nil {
|
||||
o.errMutex.RUnlock()
|
||||
return 0, o.err
|
||||
}
|
||||
o.errMutex.RUnlock()
|
||||
if len(b)%8 != 0 {
|
||||
return o.closeWithError(fmt.Errorf("oto: Read buffer length must be a multiple of 8"))
|
||||
}
|
||||
samples := len(b) / 8
|
||||
if samples > len(o.tmpBuffer) {
|
||||
o.tmpBuffer = append(o.tmpBuffer, make(sointu.AudioBuffer, samples-len(o.tmpBuffer))...)
|
||||
} else if samples < len(o.tmpBuffer) {
|
||||
o.tmpBuffer = o.tmpBuffer[:samples]
|
||||
}
|
||||
err = o.audioSource.ReadAudio(o.tmpBuffer)
|
||||
if err != nil {
|
||||
return o.closeWithError(err)
|
||||
}
|
||||
for i := range o.tmpBuffer {
|
||||
binary.LittleEndian.PutUint32(b[i*8:], math.Float32bits(o.tmpBuffer[i][0]))
|
||||
binary.LittleEndian.PutUint32(b[i*8+4:], math.Float32bits(o.tmpBuffer[i][1]))
|
||||
}
|
||||
return samples * 8, nil
|
||||
}
|
||||
|
||||
func (o *OtoReader) closeWithError(err error) (int, error) {
|
||||
o.errMutex.Lock()
|
||||
defer o.errMutex.Unlock()
|
||||
if o.err == nil {
|
||||
o.err = err
|
||||
o.waitGroup.Done()
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
Reference in New Issue
Block a user