refactor(tracker): Rewrote the sequencer loop to use simple mutex

This commit is contained in:
Veikko Sariola 2020-12-29 16:30:44 +02:00
parent 8029dbd1a8
commit cd498e775b
14 changed files with 315 additions and 166 deletions

View File

@ -1,50 +0,0 @@
package oto
import (
"fmt"
"github.com/hajimehoshi/oto"
"github.com/vsariola/sointu/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("cannot convert buffer to bytes: %w", err)
} else if _, err := o.player.Write(byteBuffer); err != nil {
return fmt.Errorf("cannot write to player: %w", err)
}
return nil
}
// Close disposes of resources
func (o *OtoPlayer) Close() error {
if err := o.player.Close(); err != nil {
return fmt.Errorf("cannot close player: %w", err)
}
if err := o.context.Close(); err != nil {
return fmt.Errorf("cannot close 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
}

View File

@ -1,6 +0,0 @@
package audio
type Player interface {
Play(buffer []float32) (err error)
Close() error
}

View File

@ -77,12 +77,18 @@ func (synth *C.Synth) Render(buffer []float32, maxtime int) (int, int, error) {
// Trigger is part of C.Synths' implementation of sointu.Synth interface // Trigger is part of C.Synths' implementation of sointu.Synth interface
func (s *C.Synth) Trigger(voice int, note byte) { func (s *C.Synth) Trigger(voice int, note byte) {
if voice < 0 || voice >= len(s.SynthWrk.Voices) {
return
}
s.SynthWrk.Voices[voice] = C.Voice{} s.SynthWrk.Voices[voice] = C.Voice{}
s.SynthWrk.Voices[voice].Note = C.int(note) s.SynthWrk.Voices[voice].Note = C.int(note)
} }
// Release is part of C.Synths' implementation of sointu.Synth interface // Release is part of C.Synths' implementation of sointu.Synth interface
func (s *C.Synth) Release(voice int) { func (s *C.Synth) Release(voice int) {
if voice < 0 || voice >= len(s.SynthWrk.Voices) {
return
}
s.SynthWrk.Voices[voice].Release = 1 s.SynthWrk.Voices[voice].Release = 1
} }

View File

@ -15,8 +15,8 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/vsariola/sointu" "github.com/vsariola/sointu"
"github.com/vsariola/sointu/audio/oto"
"github.com/vsariola/sointu/bridge" "github.com/vsariola/sointu/bridge"
"github.com/vsariola/sointu/oto"
) )
func main() { func main() {
@ -39,6 +39,15 @@ func main() {
if !*rawOut && !*wavOut { if !*rawOut && !*wavOut {
*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
if *play {
audioContext, err := oto.NewContext()
if err != nil {
fmt.Fprintf(os.Stderr, "could not acquire oto AudioContext: %v\n", err)
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 {
if *stdout { if *stdout {
@ -81,12 +90,9 @@ func main() {
return fmt.Errorf("sointu.Play failed: %v", err) return fmt.Errorf("sointu.Play failed: %v", err)
} }
if *play { if *play {
player, err := oto.NewPlayer() output := audioContext.Output()
if err != nil { defer output.Close()
return fmt.Errorf("error creating oto player: %v", err) if err := output.WriteAudio(buffer); err != nil {
}
defer player.Close()
if err := player.Play(buffer); err != nil {
return fmt.Errorf("error playing: %v", err) return fmt.Errorf("error playing: %v", err)
} }
} }

View File

@ -2,26 +2,27 @@ package main
import ( import (
"fmt" "fmt"
"os"
"gioui.org/app" "gioui.org/app"
"gioui.org/unit" "gioui.org/unit"
"github.com/vsariola/sointu/audio/oto" "github.com/vsariola/sointu/oto"
"github.com/vsariola/sointu/tracker" "github.com/vsariola/sointu/tracker"
"os"
) )
func main() { func main() {
plr, err := oto.NewPlayer() audioContext, err := oto.NewContext()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
defer plr.Close() defer audioContext.Close()
go func() { go func() {
w := app.NewWindow( w := app.NewWindow(
app.Size(unit.Dp(800), unit.Dp(600)), app.Size(unit.Dp(800), unit.Dp(600)),
app.Title("Sointu Tracker"), app.Title("Sointu Tracker"),
) )
t := tracker.New(plr) t := tracker.New(audioContext)
defer t.Close() defer t.Close()
if err := t.Run(w); err != nil { if err := t.Run(w); err != nil {
fmt.Println(err) fmt.Println(err)

1
go.mod
View File

@ -12,6 +12,7 @@ require (
github.com/huandu/xstrings v1.3.2 // indirect github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.11 // indirect github.com/imdario/mergo v0.3.11 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/stretchr/testify v1.6.1 // indirect
gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
) )

10
go.sum
View File

@ -8,6 +8,8 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
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=
@ -21,6 +23,11 @@ github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMK
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -32,6 +39,7 @@ golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+o
golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34= golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34=
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -47,8 +55,10 @@ golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,4 +1,4 @@
package audio package oto
import ( import (
"bytes" "bytes"
@ -13,7 +13,14 @@ import (
func FloatBufferTo16BitLE(buff []float32) ([]byte, error) { func FloatBufferTo16BitLE(buff []float32) ([]byte, error) {
var buf bytes.Buffer var buf bytes.Buffer
for i, v := range buff { for i, v := range buff {
uv := int16(v * math.MaxInt16) var uv int16
if v < -1.0 {
uv = -math.MaxInt16
} else if v > 1.0 {
uv = math.MaxInt16
} else {
uv = int16(v * math.MaxInt16)
}
if err := binary.Write(&buf, binary.LittleEndian, uv); err != nil { 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 nil, fmt.Errorf("error converting buffer (@ %v, value %v) to bytes: %w", i, v, err)
} }

View File

@ -1,14 +1,16 @@
package audio package oto_test
import ( import (
"reflect" "reflect"
"testing" "testing"
"github.com/vsariola/sointu/oto"
) )
func TestFloatBufferToBytes(t *testing.T) { 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} 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} 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) converted, err := oto.FloatBufferTo16BitLE(floats)
if err != nil { if err != nil {
t.Fatalf("FloatBufferTo16BitLE threw an error") t.Fatalf("FloatBufferTo16BitLE threw an error")
} }
@ -32,7 +34,7 @@ func TestFloatBufferToBytesLimits(t *testing.T) {
0xDE, 0x7F, // float 0.999 = 0x7FDE = 0111111111011110 0xDE, 0x7F, // float 0.999 = 0x7FDE = 0111111111011110
0x22, 0x80, // float -0.999 = 0x8022 = 1000000000100010 0x22, 0x80, // float -0.999 = 0x8022 = 1000000000100010
} }
converted, err := FloatBufferTo16BitLE(floats) converted, err := oto.FloatBufferTo16BitLE(floats)
if err != nil { if err != nil {
t.Fatalf("FloatBufferTo16BitLE threw an error") t.Fatalf("FloatBufferTo16BitLE threw an error")
} }

51
oto/oto.go Normal file
View File

@ -0,0 +1,51 @@
package oto
import (
"fmt"
"github.com/hajimehoshi/oto"
"github.com/vsariola/sointu"
)
type OtoContext oto.Context
type OtoOutput oto.Player
func (c *OtoContext) Output() sointu.AudioSink {
return (*OtoOutput)((*oto.Context)(c).NewPlayer())
}
const otoBufferSize = 8192
// NewPlayer creates and initializes a new OtoPlayer
func NewContext() (*OtoContext, error) {
context, err := oto.NewContext(44100, 2, 2, otoBufferSize)
if err != nil {
return nil, fmt.Errorf("cannot create oto context: %w", err)
}
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
}
// Play implements the audio.Player interface for OtoPlayer
func (o *OtoOutput) WriteAudio(floatBuffer []float32) (err error) {
if byteBuffer, err := FloatBufferTo16BitLE(floatBuffer); err != nil {
return fmt.Errorf("cannot convert buffer to bytes: %w", err)
} else if _, err := (*oto.Player)(o).Write(byteBuffer); err != nil {
return fmt.Errorf("cannot write to player: %w", err)
}
return nil
}
// Close disposes of resources
func (o *OtoOutput) Close() error {
if err := (*oto.Player)(o).Close(); err != nil {
return fmt.Errorf("cannot close oto player: %w", err)
}
return nil
}

View File

@ -76,6 +76,21 @@ func Render(synth Synth, buffer []float32) error {
return nil return nil
} }
type AudioSink interface {
WriteAudio(buffer []float32) (err error)
Close() error
}
type AudioSource interface {
ReadAudio(buffer []float32) (n int, err error)
Close() error
}
type AudioContext interface {
Output() AudioSink
Close() error
}
// UnitParameter documents one parameter that an unit takes // UnitParameter documents one parameter that an unit takes
type UnitParameter struct { type UnitParameter struct {
Name string // thould be found with this name in the Unit.Parameters map Name string // thould be found with this name in the Unit.Parameters map

View File

@ -13,6 +13,8 @@ func (t *Tracker) Layout(gtx layout.Context) {
func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions { func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
flexTracks := make([]layout.FlexChild, len(t.song.Tracks)) flexTracks := make([]layout.FlexChild, len(t.song.Tracks))
t.playRowPatMutex.RLock()
defer t.playRowPatMutex.RUnlock()
for i, trk := range t.song.Tracks { for i, trk := range t.song.Tracks {
flexTracks[i] = layout.Rigid(Lowered(t.layoutTrack( flexTracks[i] = layout.Rigid(Lowered(t.layoutTrack(
t.song.Patterns[trk.Sequence[t.DisplayPattern]], t.song.Patterns[trk.Sequence[t.DisplayPattern]],

View File

@ -1,89 +1,124 @@
package tracker package tracker
import ( import (
"errors"
"fmt" "fmt"
"sync/atomic" "math"
"time" "sync"
"github.com/vsariola/sointu"
) )
func (t *Tracker) TogglePlay() { // how many times the sequencer tries to fill the buffer. If the buffer is not
t.Playing = !t.Playing // filled after this many tries, there's probably an issue with rowlength (e.g.
t.setPlaying <- t.Playing // infinite BPM, rowlength = 0) or something else, so we error instead of
// letting ReadAudio hang.
const SEQUENCER_MAX_READ_TRIES = 1000
// Sequencer is a AudioSource that uses the given synth to render audio. In
// periods of rowLength, it pulls new notes to trigger/release from the given
// iterator. Note that the iterator should be thread safe, as the ReadAudio
// might be called from another go routine.
type Sequencer struct {
// we use mutex to ensure that voices are not triggered during readaudio or
// that the synth is not changed when audio is being read
mutex sync.Mutex
synth sointu.Synth
// this iterator is a bit unconventional in the sense that it might return
// hasNext false, but might still return hasNext true in future attempts if
// new rows become available.
iterator func() ([]Note, bool)
rowTime int
rowLength int
} }
// sequencerLoop is the main goroutine that handles the playing logic type Note struct {
func (t *Tracker) sequencerLoop(closer chan struct{}) { Voice int
playing := false Note byte
rowTime := (time.Second * 60) / time.Duration(4*t.song.BPM)
tick := make(<-chan time.Time)
curVoices := make([]int, len(t.song.Tracks))
for i := range curVoices {
curVoices[i] = t.song.FirstTrackVoice(i)
}
for {
select {
case <-tick:
next := time.Now().Add(rowTime)
pattern := atomic.LoadInt32(&t.PlayPattern)
row := atomic.LoadInt32(&t.PlayRow)
if int(row+1) == t.song.PatternRows() {
if int(pattern+1) == t.song.SequenceLength() {
atomic.StoreInt32(&t.PlayPattern, 0)
} else {
atomic.AddInt32(&t.PlayPattern, 1)
}
atomic.StoreInt32(&t.PlayRow, 0)
} else {
atomic.AddInt32(&t.PlayRow, 1)
}
if playing {
tick = time.After(next.Sub(time.Now()))
}
t.playRow(curVoices)
t.ticked <- struct{}{}
// TODO: maybe refactor the controls to be nicer, somehow?
case rowJump := <-t.rowJump:
atomic.StoreInt32(&t.PlayRow, int32(rowJump))
case patternJump := <-t.patternJump:
atomic.StoreInt32(&t.PlayPattern, int32(patternJump))
case <-closer:
return
case playState := <-t.setPlaying:
playing = playState
if playing {
t.playBuffer = make([]float32, t.song.SamplesPerRow())
tick = time.After(0)
}
} }
func NewSequencer(synth sointu.Synth, rowLength int, iterator func() ([]Note, bool)) *Sequencer {
return &Sequencer{
synth: synth,
iterator: iterator,
rowLength: rowLength,
rowTime: math.MaxInt32,
} }
} }
// playRow renders and writes the current row func (s *Sequencer) ReadAudio(buffer []float32) (int, error) {
func (t *Tracker) playRow(curVoices []int) { s.mutex.Lock()
pattern := atomic.LoadInt32(&t.PlayPattern) defer s.mutex.Unlock()
row := atomic.LoadInt32(&t.PlayRow) if s.synth == nil {
for i, trk := range t.song.Tracks { return 0, errors.New("cannot Sequencer.ReadAudio; synth is nil")
patternIndex := trk.Sequence[pattern]
note := t.song.Patterns[patternIndex][row]
if note == 1 { // anything but hold causes an action.
continue // TODO: can hold be actually something else than 1?
} }
t.synth.Release(curVoices[i]) totalRendered := 0
if note > 1 { for i := 0; i < SEQUENCER_MAX_READ_TRIES; i++ {
curVoices[i]++ gotRow := true
first := t.song.FirstTrackVoice(i) if s.rowTime >= s.rowLength {
if curVoices[i] >= first+trk.NumVoices { var row []Note
curVoices[i] = first row, gotRow = s.iterator()
if gotRow {
for _, n := range row {
s.doNote(n.Voice, n.Note)
} }
t.synth.Trigger(curVoices[i], note) s.rowTime = 0
} else {
for i := 0; i < 32; i++ {
s.doNote(i, 0)
} }
} }
buff := make([]float32, t.song.SamplesPerRow()*2) }
rendered, timeAdvanced, _ := t.synth.Render(buff, t.song.SamplesPerRow()) rowTimeRemaining := s.rowLength - s.rowTime
err := t.player.Play(buff) if !gotRow {
rowTimeRemaining = math.MaxInt32
}
rendered, timeAdvanced, err := s.synth.Render(buffer[totalRendered*2:], rowTimeRemaining)
totalRendered += rendered
s.rowTime += timeAdvanced
if err != nil { if err != nil {
fmt.Println("error playing: %w", err) return totalRendered * 2, fmt.Errorf("synth.Render failed: %v", err)
} else if timeAdvanced != t.song.SamplesPerRow() { }
fmt.Println("rendered only", rendered, "/", timeAdvanced, "expected", t.song.SamplesPerRow()) if totalRendered*2 >= len(buffer) {
return totalRendered * 2, nil
}
}
return totalRendered * 2, fmt.Errorf("despite %v attempts, Sequencer.ReadAudio could not fill the buffer (rowLength was %v, should be >> 0)", SEQUENCER_MAX_READ_TRIES, s.rowLength)
}
// Sets the synth used by the sequencer. This takes ownership of the synth: the
// synth should not be called by anyone else than the sequencer afterwards
func (s *Sequencer) SetSynth(synth sointu.Synth) {
s.mutex.Lock()
s.synth = synth
s.mutex.Unlock()
}
func (s *Sequencer) SetRowLength(rowLength int) {
s.mutex.Lock()
s.rowLength = rowLength
s.mutex.Unlock()
}
// Trigger is used to manually play a note on the sequencer when jamming. It is
// thread-safe.
func (s *Sequencer) Trigger(voice int, note byte) {
s.mutex.Lock()
s.doNote(voice, note)
s.mutex.Unlock()
}
// Release is used to manually release a note on the sequencer when jamming. It
// is thread-safe.
func (s *Sequencer) Release(voice int) {
s.Trigger(voice, 0)
}
// doNote is the internal trigger/release function that is not thread safe
func (s *Sequencer) doNote(voice int, note byte) {
if note == 0 {
s.synth.Release(voice)
} else {
s.synth.Trigger(voice, note)
} }
} }

View File

@ -2,28 +2,33 @@ package tracker
import ( import (
"fmt" "fmt"
"sync"
"gioui.org/widget" "gioui.org/widget"
"github.com/vsariola/sointu" "github.com/vsariola/sointu"
"github.com/vsariola/sointu/audio"
"github.com/vsariola/sointu/bridge" "github.com/vsariola/sointu/bridge"
) )
type Tracker struct { type Tracker struct {
QuitButton *widget.Clickable QuitButton *widget.Clickable
songPlayMutex sync.RWMutex // protects song and playing
song sointu.Song song sointu.Song
Playing bool
// protects PlayPattern and PlayRow
playRowPatMutex sync.RWMutex // protects song and playing
PlayPattern int
PlayRow int
CursorRow int CursorRow int
CursorColumn int CursorColumn int
DisplayPattern int DisplayPattern int
PlayPattern int32
PlayRow int32
ActiveTrack int ActiveTrack int
CurrentOctave byte CurrentOctave byte
Playing bool
ticked chan struct{} ticked chan struct{}
setPlaying chan bool setPlaying chan bool
rowJump chan int rowJump chan int
patternJump chan int patternJump chan int
player audio.Player audioContext sointu.AudioContext
synth sointu.Synth synth sointu.Synth
playBuffer []float32 playBuffer []float32
closer chan struct{} closer chan struct{}
@ -33,6 +38,8 @@ func (t *Tracker) LoadSong(song sointu.Song) error {
if err := song.Validate(); err != nil { if err := song.Validate(); err != nil {
return fmt.Errorf("invalid song: %w", err) return fmt.Errorf("invalid song: %w", err)
} }
t.songPlayMutex.Lock()
defer t.songPlayMutex.Unlock()
t.song = song t.song = song
if synth, err := bridge.Synth(song.Patch); err != nil { if synth, err := bridge.Synth(song.Patch); err != nil {
fmt.Printf("error loading synth: %v\n", err) fmt.Printf("error loading synth: %v\n", err)
@ -44,15 +51,77 @@ func (t *Tracker) LoadSong(song sointu.Song) error {
} }
func (t *Tracker) Close() { func (t *Tracker) Close() {
t.player.Close() t.audioContext.Close()
t.closer <- struct{}{} t.closer <- struct{}{}
} }
func New(player audio.Player) *Tracker { func (t *Tracker) TogglePlay() {
t.songPlayMutex.Lock()
defer t.songPlayMutex.Unlock()
t.Playing = !t.Playing
}
func (t *Tracker) sequencerLoop(closer <-chan struct{}) {
output := t.audioContext.Output()
defer output.Close()
synth, err := bridge.Synth(t.song.Patch)
if err != nil {
panic("cannot create a synth with the default patch")
}
curVoices := make([]int, 32)
sequencer := NewSequencer(synth, 44100*60/(4*t.song.BPM), func() ([]Note, bool) {
t.songPlayMutex.RLock()
defer t.songPlayMutex.RUnlock()
if !t.Playing {
return nil, false
}
t.playRowPatMutex.Lock()
defer t.playRowPatMutex.Unlock()
t.PlayRow++
if t.PlayRow >= t.song.PatternRows() {
t.PlayRow = 0
t.PlayPattern++
}
if t.PlayPattern >= t.song.SequenceLength() {
t.PlayPattern = 0
}
notes := make([]Note, 0, 32)
for track := range t.song.Tracks {
patternIndex := t.song.Tracks[track].Sequence[t.PlayPattern]
note := t.song.Patterns[patternIndex][t.PlayRow]
if note == 1 { // anything but hold causes an action.
continue
}
notes = append(notes, Note{curVoices[track], 0})
if note > 1 {
curVoices[track]++
first := t.song.FirstTrackVoice(track)
if curVoices[track] >= first+t.song.Tracks[track].NumVoices {
curVoices[track] = first
}
notes = append(notes, Note{curVoices[track], note})
}
}
t.ticked <- struct{}{}
return notes, true
})
buffer := make([]float32, 8192)
for {
select {
case <-closer:
return
default:
sequencer.ReadAudio(buffer)
output.WriteAudio(buffer)
}
}
}
func New(audioContext sointu.AudioContext) *Tracker {
t := &Tracker{ t := &Tracker{
QuitButton: new(widget.Clickable), QuitButton: new(widget.Clickable),
CurrentOctave: 4, CurrentOctave: 4,
player: player, audioContext: audioContext,
setPlaying: make(chan bool), setPlaying: make(chan bool),
rowJump: make(chan int), rowJump: make(chan int),
patternJump: make(chan int), patternJump: make(chan int),