feat: upgrade oto and output float audio

This commit is contained in:
5684185+vsariola@users.noreply.github.com 2024-10-05 19:48:30 +03:00
parent 890ebe3294
commit 81a6d1acea
9 changed files with 146 additions and 153 deletions

View File

@ -21,6 +21,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
wrong scaling ([#150][i150])
- Empty patch should not crash the native synth ([#148][i148])
### Changed
- The stand-alone apps now output floating point sound, as made possible by
upgrading oto-library to latest version. This way the tracker sound output
matches the compiled output better, as usually compiled intros output sound in
floating point. This might be important if OS sound drivers apply some audio
enhancemenets e.g. compressors to the audio.
## [0.4.1]
### Added
- Clicking the parameter slider also selects that parameter ([#112][i112])

View File

@ -5,6 +5,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"io"
"math"
)
@ -13,22 +14,30 @@ type (
// sample represented by [2]float32. [0] is left channel, [1] is right
AudioBuffer [][2]float32
// AudioOutput represents something where we can send audio e.g. audio output.
// WriteAudio should block if not ready to accept audio e.g. buffer full.
AudioOutput interface {
WriteAudio(buffer AudioBuffer) error
Close() error
CloserWaiter interface {
io.Closer
Wait()
}
// AudioContext represents the low-level audio drivers. There should be at most
// one AudioContext at a time. The interface is implemented at least by
// AudioContext represents the low-level audio drivers. There should be at
// most one AudioContext at a time. The interface is implemented at least by
// oto.OtoContext, but in future we could also mock it.
//
// AudioContext is used to create one or more AudioOutputs with Output(); each
// can be used to output separate sound & closed when done.
// AudioContext is used to play one or more AudioSources. Playing can be
// stopped by closing the returned io.Closer.
AudioContext interface {
Output() AudioOutput
Close() error
Play(r AudioSource) CloserWaiter
}
// AudioSource is an interface for reading audio samples into an
// AudioBuffer. Returns error if the buffer is not filled.
AudioSource interface {
ReadAudio(buf AudioBuffer) error
}
BufferSource struct {
buffer AudioBuffer
pos int
}
// Synth represents a state of a synthesizer, compiled from a Patch.
@ -145,6 +154,21 @@ func (buffer AudioBuffer) Fill(synth Synth) error {
return nil
}
func (b AudioBuffer) Source() *BufferSource {
return &BufferSource{buffer: b}
}
// ReadAudio reads audio samples from an AudioSource into an AudioBuffer.
// Returns an error when the buffer is fully consumed.
func (a *BufferSource) ReadAudio(buf AudioBuffer) error {
n := copy(buf, a.buffer[a.pos:])
a.pos += n
if a.pos >= len(a.buffer) {
return io.EOF
}
return nil
}
// Wav converts an AudioBuffer into a valid WAV-file, returned as a []byte
// array.
//

View File

@ -43,6 +43,7 @@ func main() {
*play = true // if the user gives nothing to output, then the default behaviour is just to play the file
}
var audioContext sointu.AudioContext
var playWaiter sointu.CloserWaiter
if *play {
var err error
audioContext, err = oto.NewContext()
@ -50,7 +51,6 @@ func main() {
fmt.Fprintf(os.Stderr, "could not acquire oto AudioContext: %v\n", err)
os.Exit(1)
}
defer audioContext.Close()
}
process := func(filename string) error {
output := func(extension string, contents []byte) error {
@ -98,11 +98,7 @@ func main() {
return fmt.Errorf("sointu.Play failed: %v", err)
}
if *play {
output := audioContext.Output()
defer output.Close()
if err := output.WriteAudio(buffer); err != nil {
return fmt.Errorf("error playing: %v", err)
}
playWaiter = audioContext.Play(buffer.Source())
}
if *rawOut {
raw, err := buffer.Raw(*pcm)
@ -122,6 +118,9 @@ func main() {
return fmt.Errorf("error outputting .wav file: %v", err)
}
}
if *play {
playWaiter.Wait()
}
return nil
}
retval := 0

View File

@ -28,6 +28,16 @@ func (NullContext) BPM() (bpm float64, ok bool) {
return 0, false
}
type PlayerAudioSource struct {
*tracker.Player
playerProcessContext tracker.PlayerProcessContext
}
func (p *PlayerAudioSource) ReadAudio(buf sointu.AudioBuffer) error {
p.Player.Process(buf, p.playerProcessContext)
return nil
}
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
@ -49,7 +59,6 @@ func main() {
fmt.Println(err)
os.Exit(1)
}
defer audioContext.Close()
recoveryFile := ""
if configDir, err := os.UserConfigDir(); err == nil {
recoveryFile = filepath.Join(configDir, "Sointu", "sointu-track-recovery")
@ -63,18 +72,10 @@ func main() {
f.Close()
}
tracker := gioui.NewTracker(model)
output := audioContext.Output()
defer output.Close()
go func() {
buf := make(sointu.AudioBuffer, 1024)
ctx := NullContext{}
for {
player.Process(buf, ctx)
output.WriteAudio(buf)
}
}()
audioCloser := audioContext.Play(&PlayerAudioSource{player, NullContext{}})
go func() {
tracker.Main()
audioCloser.Close()
if *cpuprofile != "" {
pprof.StopCPUProfile()
f.Close()

6
go.mod
View File

@ -6,7 +6,7 @@ require (
gioui.org v0.7.1
gioui.org/x v0.7.1
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/hajimehoshi/oto v0.6.6
github.com/ebitengine/oto/v3 v3.3.0
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37
golang.org/x/text v0.16.0
gopkg.in/yaml.v2 v2.3.0
@ -20,6 +20,7 @@ require (
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect
github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/ebitengine/purego v0.8.0 // indirect
github.com/go-text/typesetting v0.1.1 // indirect
github.com/godbus/dbus/v5 v5.0.6 // indirect
github.com/google/uuid v1.1.2 // indirect
@ -31,8 +32,7 @@ require (
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/sys v0.25.0 // indirect
pipelined.dev/pipe v0.11.0 // indirect
pipelined.dev/signal v0.10.0 // indirect
)

20
go.sum
View File

@ -19,6 +19,12 @@ github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZC
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/oto/v3 v3.3.0 h1:34lJpJLqda0Iee9g9p8RWtVVwBcOOO2YSIS2x4yD1OQ=
github.com/ebitengine/oto/v3 v3.3.0/go.mod h1:MZeb/lwoC4DCOdiTIxYezrURTw7EvK/yF863+tmBI+U=
github.com/ebitengine/oto/v3 v3.3.1 h1:d4McwGQuXOT0GL7bA5g9ZnaUEIEjQvG3hafzMy+T3qE=
github.com/ebitengine/oto/v3 v3.3.1/go.mod h1:MZeb/lwoC4DCOdiTIxYezrURTw7EvK/yF863+tmBI+U=
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
@ -27,8 +33,6 @@ github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hajimehoshi/oto v0.6.6 h1:HYSZ8cYZqOL4iHugvbcfhNN2smiSOsBMaoSBi4nnWcw=
github.com/hajimehoshi/oto v0.6.6/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
@ -44,22 +48,14 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg=
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc=
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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@ -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)
}

View File

@ -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")
}
}

View File

@ -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
}