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,89 +1,124 @@
package tracker
import (
"errors"
"fmt"
"sync/atomic"
"time"
"math"
"sync"
"github.com/vsariola/sointu"
)
func (t *Tracker) TogglePlay() {
t.Playing = !t.Playing
t.setPlaying <- t.Playing
// how many times the sequencer tries to fill the buffer. If the buffer is not
// filled after this many tries, there's probably an issue with rowlength (e.g.
// 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
func (t *Tracker) sequencerLoop(closer chan struct{}) {
playing := false
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)
type Note struct {
Voice int
Note byte
}
func NewSequencer(synth sointu.Synth, rowLength int, iterator func() ([]Note, bool)) *Sequencer {
return &Sequencer{
synth: synth,
iterator: iterator,
rowLength: rowLength,
rowTime: math.MaxInt32,
}
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)
}
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
if s.rowTime >= s.rowLength {
var row []Note
row, gotRow = s.iterator()
if gotRow {
for _, n := range row {
s.doNote(n.Voice, n.Note)
}
atomic.StoreInt32(&t.PlayRow, 0)
s.rowTime = 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)
for i := 0; i < 32; i++ {
s.doNote(i, 0)
}
}
}
rowTimeRemaining := s.rowLength - s.rowTime
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 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)
}
// playRow renders and writes the current row
func (t *Tracker) playRow(curVoices []int) {
pattern := atomic.LoadInt32(&t.PlayPattern)
row := atomic.LoadInt32(&t.PlayRow)
for i, trk := range t.song.Tracks {
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])
if note > 1 {
curVoices[i]++
first := t.song.FirstTrackVoice(i)
if curVoices[i] >= first+trk.NumVoices {
curVoices[i] = first
}
t.synth.Trigger(curVoices[i], note)
}
}
buff := make([]float32, t.song.SamplesPerRow()*2)
rendered, timeAdvanced, _ := t.synth.Render(buff, t.song.SamplesPerRow())
err := t.player.Play(buff)
if err != nil {
fmt.Println("error playing: %w", err)
} else if timeAdvanced != t.song.SamplesPerRow() {
fmt.Println("rendered only", rendered, "/", timeAdvanced, "expected", t.song.SamplesPerRow())
// 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)
}
}