feat(sointu): add SynthService for recompiling the synth when needed

This commit is contained in:
vsariola 2021-02-05 22:21:46 +02:00
parent 6307dd51de
commit 5e7bd75b36
5 changed files with 85 additions and 62 deletions

View File

@ -13,6 +13,14 @@ import (
"github.com/vsariola/sointu/compiler"
)
type BridgeService struct {
}
func (s BridgeService) Compile(patch sointu.Patch) (sointu.Synth, error) {
synth, err := Synth(patch)
return synth, err
}
func Synth(patch sointu.Patch) (*C.Synth, error) {
s := new(C.Synth)
comPatch, err := compiler.Encode(&patch, compiler.AllFeatures{})

View File

@ -6,6 +6,7 @@ import (
"gioui.org/app"
"gioui.org/unit"
"github.com/vsariola/sointu/bridge"
"github.com/vsariola/sointu/oto"
"github.com/vsariola/sointu/tracker"
)
@ -17,12 +18,13 @@ func main() {
os.Exit(1)
}
defer audioContext.Close()
synthService := bridge.BridgeService{}
go func() {
w := app.NewWindow(
app.Size(unit.Dp(800), unit.Dp(600)),
app.Title("Sointu Tracker"),
)
t := tracker.New(audioContext)
t := tracker.New(audioContext, synthService)
defer t.Close()
if err := t.Run(w); err != nil {
fmt.Println(err)

View File

@ -120,6 +120,10 @@ func Render(synth Synth, buffer []float32) error {
return nil
}
type SynthService interface {
Compile(patch Patch) (Synth, error)
}
type AudioSink interface {
WriteAudio(buffer []float32) (err error)
Close() error

View File

@ -1,7 +1,6 @@
package tracker
import (
"errors"
"fmt"
"math"
"sync"
@ -22,8 +21,9 @@ const SEQUENCER_MAX_READ_TRIES = 1000
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
mutex sync.Mutex
synth sointu.Synth
service sointu.SynthService
// 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.
@ -37,11 +37,11 @@ type Note struct {
Note byte
}
func NewSequencer(synth sointu.Synth, rowLength int, iterator func() ([]Note, bool)) *Sequencer {
func NewSequencer(service sointu.SynthService, iterator func() ([]Note, bool)) *Sequencer {
return &Sequencer{
synth: synth,
service: service,
iterator: iterator,
rowLength: rowLength,
rowLength: math.MaxInt32,
rowTime: math.MaxInt32,
}
}
@ -49,9 +49,6 @@ func NewSequencer(synth sointu.Synth, rowLength int, iterator func() ([]Note, bo
func (s *Sequencer) ReadAudio(buffer []float32) (int, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.synth == nil {
return 0, errors.New("cannot Sequencer.ReadAudio; synth is nil")
}
totalRendered := 0
for i := 0; i < SEQUENCER_MAX_READ_TRIES; i++ {
gotRow := true
@ -75,11 +72,21 @@ func (s *Sequencer) ReadAudio(buffer []float32) (int, error) {
if !gotRow {
rowTimeRemaining = math.MaxInt32
}
rendered, timeAdvanced, err := s.synth.Render(buffer[totalRendered*2:], rowTimeRemaining)
totalRendered += rendered
s.rowTime += timeAdvanced
if err != nil {
return totalRendered * 2, fmt.Errorf("synth.Render failed: %v", err)
if s.synth != nil {
rendered, timeAdvanced, err := s.synth.Render(buffer[totalRendered*2:], rowTimeRemaining)
if err != nil {
s.synth = nil
}
totalRendered += rendered
s.rowTime += timeAdvanced
} else {
for totalRendered*2 < len(buffer) && rowTimeRemaining > 0 {
buffer[totalRendered*2] = 0
buffer[totalRendered*2+1] = 0
totalRendered++
s.rowTime++
rowTimeRemaining--
}
}
if totalRendered*2 >= len(buffer) {
return totalRendered * 2, nil
@ -93,7 +100,9 @@ func (s *Sequencer) SetPatch(patch sointu.Patch) {
s.mutex.Lock()
if s.synth != nil {
s.synth.Update(patch)
} // TODO: what is s.synth is nil?
} else {
s.synth, _ = s.service.Compile(patch)
}
s.mutex.Unlock()
}
@ -119,9 +128,11 @@ func (s *Sequencer) Release(voice int) {
// 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)
if s.synth != nil {
if note == 0 {
s.synth.Release(voice)
} else {
s.synth.Trigger(voice, note)
}
}
}

View File

@ -9,7 +9,6 @@ import (
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/bridge"
)
type Tracker struct {
@ -70,7 +69,6 @@ type Tracker struct {
rowJump chan int
patternJump chan int
audioContext sointu.AudioContext
synth sointu.Synth
playBuffer []float32
closer chan struct{}
undoStack []sointu.Song
@ -111,51 +109,17 @@ func (t *Tracker) TogglePlay() {
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)
t.sequencer = NewSequencer(synth, t.song.SamplesPerRow(), func() ([]Note, bool) {
t.playRowPatMutex.Lock()
if !t.Playing {
t.playRowPatMutex.Unlock()
return nil, false
}
t.PlayPosition.Row++
t.PlayPosition.Wrap(t.song)
if t.NoteTracking {
t.Cursor.SongRow = t.PlayPosition
t.SelectionCorner.SongRow = t.PlayPosition
}
notes := make([]Note, 0, 32)
for track := range t.song.Tracks {
patternIndex := t.song.Tracks[track].Sequence[t.PlayPosition.Pattern]
note := t.song.Tracks[track].Patterns[patternIndex][t.PlayPosition.Row]
if note == 1 { // anything but hold causes an action.
continue
}
first := t.song.FirstTrackVoice(track)
notes = append(notes, Note{first + curVoices[track], 0})
if note > 1 {
curVoices[track]++
if curVoices[track] >= t.song.Tracks[track].NumVoices {
curVoices[track] = 0
}
notes = append(notes, Note{first + curVoices[track], note})
}
}
t.playRowPatMutex.Unlock()
t.ticked <- struct{}{}
return notes, true
})
buffer := make([]float32, 8192)
for {
select {
case <-closer:
return
default:
t.sequencer.ReadAudio(buffer)
read, _ := t.sequencer.ReadAudio(buffer)
for read < len(buffer) {
buffer[read] = 0
read++
}
output.WriteAudio(buffer)
}
}
@ -448,7 +412,7 @@ func (t *Tracker) DeleteSelection() {
}
}
func New(audioContext sointu.AudioContext) *Tracker {
func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tracker {
t := &Tracker{
Theme: material.NewTheme(gofont.Collection()),
QuitButton: new(widget.Clickable),
@ -495,6 +459,40 @@ func New(audioContext sointu.AudioContext) *Tracker {
for range allUnits {
t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable))
}
curVoices := make([]int, 32)
t.sequencer = NewSequencer(synthService, func() ([]Note, bool) {
t.playRowPatMutex.Lock()
if !t.Playing {
t.playRowPatMutex.Unlock()
return nil, false
}
t.PlayPosition.Row++
t.PlayPosition.Wrap(t.song)
if t.NoteTracking {
t.Cursor.SongRow = t.PlayPosition
t.SelectionCorner.SongRow = t.PlayPosition
}
notes := make([]Note, 0, 32)
for track := range t.song.Tracks {
patternIndex := t.song.Tracks[track].Sequence[t.PlayPosition.Pattern]
note := t.song.Tracks[track].Patterns[patternIndex][t.PlayPosition.Row]
if note == 1 { // anything but hold causes an action.
continue
}
first := t.song.FirstTrackVoice(track)
notes = append(notes, Note{first + curVoices[track], 0})
if note > 1 {
curVoices[track]++
if curVoices[track] >= t.song.Tracks[track].NumVoices {
curVoices[track] = 0
}
notes = append(notes, Note{first + curVoices[track], note})
}
}
t.playRowPatMutex.Unlock()
t.ticked <- struct{}{}
return notes, true
})
go t.sequencerLoop(t.closer)
if err := t.LoadSong(defaultSong.Copy()); err != nil {
panic(fmt.Errorf("cannot load default song: %w", err))