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" "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) { func Synth(patch sointu.Patch) (*C.Synth, error) {
s := new(C.Synth) s := new(C.Synth)
comPatch, err := compiler.Encode(&patch, compiler.AllFeatures{}) comPatch, err := compiler.Encode(&patch, compiler.AllFeatures{})

View File

@ -6,6 +6,7 @@ import (
"gioui.org/app" "gioui.org/app"
"gioui.org/unit" "gioui.org/unit"
"github.com/vsariola/sointu/bridge"
"github.com/vsariola/sointu/oto" "github.com/vsariola/sointu/oto"
"github.com/vsariola/sointu/tracker" "github.com/vsariola/sointu/tracker"
) )
@ -17,12 +18,13 @@ func main() {
os.Exit(1) os.Exit(1)
} }
defer audioContext.Close() defer audioContext.Close()
synthService := bridge.BridgeService{}
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(audioContext) t := tracker.New(audioContext, synthService)
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)

View File

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

View File

@ -1,7 +1,6 @@
package tracker package tracker
import ( import (
"errors"
"fmt" "fmt"
"math" "math"
"sync" "sync"
@ -24,6 +23,7 @@ type Sequencer struct {
// that the synth is not changed when audio is being read // that the synth is not changed when audio is being read
mutex sync.Mutex mutex sync.Mutex
synth sointu.Synth synth sointu.Synth
service sointu.SynthService
// this iterator is a bit unconventional in the sense that it might return // 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 // hasNext false, but might still return hasNext true in future attempts if
// new rows become available. // new rows become available.
@ -37,11 +37,11 @@ type Note struct {
Note byte 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{ return &Sequencer{
synth: synth, service: service,
iterator: iterator, iterator: iterator,
rowLength: rowLength, rowLength: math.MaxInt32,
rowTime: 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) { func (s *Sequencer) ReadAudio(buffer []float32) (int, error) {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if s.synth == nil {
return 0, errors.New("cannot Sequencer.ReadAudio; synth is nil")
}
totalRendered := 0 totalRendered := 0
for i := 0; i < SEQUENCER_MAX_READ_TRIES; i++ { for i := 0; i < SEQUENCER_MAX_READ_TRIES; i++ {
gotRow := true gotRow := true
@ -75,11 +72,21 @@ func (s *Sequencer) ReadAudio(buffer []float32) (int, error) {
if !gotRow { if !gotRow {
rowTimeRemaining = math.MaxInt32 rowTimeRemaining = math.MaxInt32
} }
if s.synth != nil {
rendered, timeAdvanced, err := s.synth.Render(buffer[totalRendered*2:], rowTimeRemaining) rendered, timeAdvanced, err := s.synth.Render(buffer[totalRendered*2:], rowTimeRemaining)
if err != nil {
s.synth = nil
}
totalRendered += rendered totalRendered += rendered
s.rowTime += timeAdvanced s.rowTime += timeAdvanced
if err != nil { } else {
return totalRendered * 2, fmt.Errorf("synth.Render failed: %v", err) 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) { if totalRendered*2 >= len(buffer) {
return totalRendered * 2, nil return totalRendered * 2, nil
@ -93,7 +100,9 @@ func (s *Sequencer) SetPatch(patch sointu.Patch) {
s.mutex.Lock() s.mutex.Lock()
if s.synth != nil { if s.synth != nil {
s.synth.Update(patch) s.synth.Update(patch)
} // TODO: what is s.synth is nil? } else {
s.synth, _ = s.service.Compile(patch)
}
s.mutex.Unlock() 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 // doNote is the internal trigger/release function that is not thread safe
func (s *Sequencer) doNote(voice int, note byte) { func (s *Sequencer) doNote(voice int, note byte) {
if s.synth != nil {
if note == 0 { if note == 0 {
s.synth.Release(voice) s.synth.Release(voice)
} else { } else {
s.synth.Trigger(voice, note) s.synth.Trigger(voice, note)
} }
}
} }

View File

@ -9,7 +9,6 @@ import (
"gioui.org/widget" "gioui.org/widget"
"gioui.org/widget/material" "gioui.org/widget/material"
"github.com/vsariola/sointu" "github.com/vsariola/sointu"
"github.com/vsariola/sointu/bridge"
) )
type Tracker struct { type Tracker struct {
@ -70,7 +69,6 @@ type Tracker struct {
rowJump chan int rowJump chan int
patternJump chan int patternJump chan int
audioContext sointu.AudioContext audioContext sointu.AudioContext
synth sointu.Synth
playBuffer []float32 playBuffer []float32
closer chan struct{} closer chan struct{}
undoStack []sointu.Song undoStack []sointu.Song
@ -111,51 +109,17 @@ func (t *Tracker) TogglePlay() {
func (t *Tracker) sequencerLoop(closer <-chan struct{}) { func (t *Tracker) sequencerLoop(closer <-chan struct{}) {
output := t.audioContext.Output() output := t.audioContext.Output()
defer output.Close() 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) buffer := make([]float32, 8192)
for { for {
select { select {
case <-closer: case <-closer:
return return
default: default:
t.sequencer.ReadAudio(buffer) read, _ := t.sequencer.ReadAudio(buffer)
for read < len(buffer) {
buffer[read] = 0
read++
}
output.WriteAudio(buffer) 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{ t := &Tracker{
Theme: material.NewTheme(gofont.Collection()), Theme: material.NewTheme(gofont.Collection()),
QuitButton: new(widget.Clickable), QuitButton: new(widget.Clickable),
@ -495,6 +459,40 @@ func New(audioContext sointu.AudioContext) *Tracker {
for range allUnits { for range allUnits {
t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable)) 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) go t.sequencerLoop(t.closer)
if err := t.LoadSong(defaultSong.Copy()); err != nil { if err := t.LoadSong(defaultSong.Copy()); err != nil {
panic(fmt.Errorf("cannot load default song: %w", err)) panic(fmt.Errorf("cannot load default song: %w", err))