sointu/tracker/sequencer.go

90 lines
2.4 KiB
Go

package tracker
import (
"fmt"
"sync/atomic"
"time"
)
func (t *Tracker) TogglePlay() {
t.Playing = !t.Playing
t.setPlaying <- t.Playing
}
// 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)
}
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)
}
atomic.StoreInt32(&t.PlayRow, 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)
}
}
}
}
// 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())
}
}