mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
feat(sointu): add SynthService for recompiling the synth when needed
This commit is contained in:
parent
6307dd51de
commit
5e7bd75b36
@ -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{})
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user