diff --git a/bridge/bridge.go b/bridge/bridge.go index e5a7f85..4cf210b 100644 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -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{}) diff --git a/cmd/sointu-track/main.go b/cmd/sointu-track/main.go index 4713edf..14c463f 100644 --- a/cmd/sointu-track/main.go +++ b/cmd/sointu-track/main.go @@ -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) diff --git a/sointu.go b/sointu.go index 5f3b422..30ae020 100644 --- a/sointu.go +++ b/sointu.go @@ -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 diff --git a/tracker/sequencer.go b/tracker/sequencer.go index e0d5124..ac6911d 100644 --- a/tracker/sequencer.go +++ b/tracker/sequencer.go @@ -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) + } } } diff --git a/tracker/tracker.go b/tracker/tracker.go index 2cdefe3..f720e66 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -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))