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]) wrong scaling ([#150][i150])
- Empty patch should not crash the native synth ([#148][i148]) - 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] ## [0.4.1]
### Added ### Added
- Clicking the parameter slider also selects that parameter ([#112][i112]) - Clicking the parameter slider also selects that parameter ([#112][i112])

View File

@ -5,6 +5,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"io"
"math" "math"
) )
@ -13,22 +14,30 @@ type (
// sample represented by [2]float32. [0] is left channel, [1] is right // sample represented by [2]float32. [0] is left channel, [1] is right
AudioBuffer [][2]float32 AudioBuffer [][2]float32
// AudioOutput represents something where we can send audio e.g. audio output. CloserWaiter interface {
// WriteAudio should block if not ready to accept audio e.g. buffer full. io.Closer
AudioOutput interface { Wait()
WriteAudio(buffer AudioBuffer) error
Close() error
} }
// AudioContext represents the low-level audio drivers. There should be at most // AudioContext represents the low-level audio drivers. There should be at
// one AudioContext at a time. The interface is implemented at least by // most one AudioContext at a time. The interface is implemented at least by
// oto.OtoContext, but in future we could also mock it. // oto.OtoContext, but in future we could also mock it.
// //
// AudioContext is used to create one or more AudioOutputs with Output(); each // AudioContext is used to play one or more AudioSources. Playing can be
// can be used to output separate sound & closed when done. // stopped by closing the returned io.Closer.
AudioContext interface { AudioContext interface {
Output() AudioOutput Play(r AudioSource) CloserWaiter
Close() error }
// 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. // Synth represents a state of a synthesizer, compiled from a Patch.
@ -145,6 +154,21 @@ func (buffer AudioBuffer) Fill(synth Synth) error {
return nil 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 // Wav converts an AudioBuffer into a valid WAV-file, returned as a []byte
// array. // 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 *play = true // if the user gives nothing to output, then the default behaviour is just to play the file
} }
var audioContext sointu.AudioContext var audioContext sointu.AudioContext
var playWaiter sointu.CloserWaiter
if *play { if *play {
var err error var err error
audioContext, err = oto.NewContext() audioContext, err = oto.NewContext()
@ -50,7 +51,6 @@ func main() {
fmt.Fprintf(os.Stderr, "could not acquire oto AudioContext: %v\n", err) fmt.Fprintf(os.Stderr, "could not acquire oto AudioContext: %v\n", err)
os.Exit(1) os.Exit(1)
} }
defer audioContext.Close()
} }
process := func(filename string) error { process := func(filename string) error {
output := func(extension string, contents []byte) error { output := func(extension string, contents []byte) error {
@ -98,11 +98,7 @@ func main() {
return fmt.Errorf("sointu.Play failed: %v", err) return fmt.Errorf("sointu.Play failed: %v", err)
} }
if *play { if *play {
output := audioContext.Output() playWaiter = audioContext.Play(buffer.Source())
defer output.Close()
if err := output.WriteAudio(buffer); err != nil {
return fmt.Errorf("error playing: %v", err)
}
} }
if *rawOut { if *rawOut {
raw, err := buffer.Raw(*pcm) raw, err := buffer.Raw(*pcm)
@ -122,6 +118,9 @@ func main() {
return fmt.Errorf("error outputting .wav file: %v", err) return fmt.Errorf("error outputting .wav file: %v", err)
} }
} }
if *play {
playWaiter.Wait()
}
return nil return nil
} }
retval := 0 retval := 0

View File

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

6
go.mod
View File

@ -6,7 +6,7 @@ require (
gioui.org v0.7.1 gioui.org v0.7.1
gioui.org/x v0.7.1 gioui.org/x v0.7.1
github.com/Masterminds/sprig v2.22.0+incompatible 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/exp/shiny v0.0.0-20240707233637-46b078467d37
golang.org/x/text v0.16.0 golang.org/x/text v0.16.0
gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v2 v2.3.0
@ -20,6 +20,7 @@ require (
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect
github.com/Masterminds/goutils v1.1.0 // indirect github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.5.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/go-text/typesetting v0.1.1 // indirect
github.com/godbus/dbus/v5 v5.0.6 // indirect github.com/godbus/dbus/v5 v5.0.6 // indirect
github.com/google/uuid v1.1.2 // 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/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
golang.org/x/image v0.18.0 // indirect golang.org/x/image v0.18.0 // indirect
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect golang.org/x/sys v0.25.0 // indirect
golang.org/x/sys v0.22.0 // indirect
pipelined.dev/pipe v0.11.0 // indirect pipelined.dev/pipe v0.11.0 // indirect
pipelined.dev/signal v0.10.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/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 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= 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= 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/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 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= 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= 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 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o= 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 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= 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/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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 package oto
import ( import (
"encoding/binary"
"errors"
"fmt" "fmt"
"math"
"sync"
"github.com/hajimehoshi/oto" "github.com/ebitengine/oto/v3"
"github.com/vsariola/sointu" "github.com/vsariola/sointu"
) )
type OtoContext oto.Context const latency = 2048 // in samples at 44100 Hz = ~46 ms
type OtoOutput struct {
player *oto.Player
tmpBuffer []byte
}
func (c *OtoContext) Output() sointu.AudioOutput { type (
return &OtoOutput{player: (*oto.Context)(c).NewPlayer(), tmpBuffer: make([]byte, 0)} 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) { 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 { if err != nil {
return nil, fmt.Errorf("cannot create oto context: %w", err) return nil, fmt.Errorf("cannot create oto context: %w", err)
} }
<-readyChan
return (*OtoContext)(context), nil return (*OtoContext)(context), nil
} }
func (c *OtoContext) Close() error { func (c *OtoContext) Play(r sointu.AudioSource) sointu.CloserWaiter {
if err := (*oto.Context)(c).Close(); err != nil { reader := &OtoReader{audioSource: r}
return fmt.Errorf("cannot close oto context: %w", err) reader.waitGroup.Add(1)
} player := (*oto.Context)(c).NewPlayer(reader)
return nil player.SetBufferSize(latency * 8)
player.Play()
return OtoPlayer{player: player, reader: reader}
} }
// Play implements the audio.Player interface for OtoPlayer func (o OtoPlayer) Wait() {
func (o *OtoOutput) WriteAudio(floatBuffer sointu.AudioBuffer) (err error) { o.reader.waitGroup.Wait()
// 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
} }
// Close disposes of resources func (o OtoPlayer) Close() error {
func (o *OtoOutput) Close() error { o.reader.closeWithError(errors.New("OtoPlayer was closed"))
if err := o.player.Close(); err != nil { return o.player.Close()
return fmt.Errorf("cannot close oto player: %w", err) }
}
return nil 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
} }