sointu/tracker/tracker.go
vsariola 5dd81430b7 feat(sointu): make patterns local to track
The global pattern table is constructed only during compilation. At this point, we can do also all sorts of optimizations / changes e.g. remove unnecessary releases and reuse patterns if there's a pattern already that could be used.
2021-01-03 01:06:59 +02:00

145 lines
3.4 KiB
Go

package tracker
import (
"fmt"
"sync"
"gioui.org/widget"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/bridge"
)
type Tracker struct {
QuitButton *widget.Clickable
songPlayMutex sync.RWMutex // protects song and playing
song sointu.Song
Playing bool
// protects PlayPattern and PlayRow
playRowPatMutex sync.RWMutex // protects song and playing
PlayPattern int
PlayRow int
CursorRow int
CursorColumn int
DisplayPattern int
ActiveTrack int
CurrentOctave byte
NoteTracking bool
ticked chan struct{}
setPlaying chan bool
rowJump chan int
patternJump chan int
audioContext sointu.AudioContext
synth sointu.Synth
playBuffer []float32
closer chan struct{}
}
func (t *Tracker) LoadSong(song sointu.Song) error {
if err := song.Validate(); err != nil {
return fmt.Errorf("invalid song: %w", err)
}
t.songPlayMutex.Lock()
defer t.songPlayMutex.Unlock()
t.song = song
if synth, err := bridge.Synth(song.Patch); err != nil {
fmt.Printf("error loading synth: %v\n", err)
t.synth = nil
} else {
t.synth = synth
}
return nil
}
func (t *Tracker) Close() {
t.audioContext.Close()
t.closer <- struct{}{}
}
func (t *Tracker) TogglePlay() {
t.songPlayMutex.Lock()
defer t.songPlayMutex.Unlock()
t.Playing = !t.Playing
if t.Playing {
t.NoteTracking = true
}
}
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)
sequencer := NewSequencer(synth, 44100*60/(4*t.song.BPM), func() ([]Note, bool) {
t.songPlayMutex.RLock()
defer t.songPlayMutex.RUnlock()
if !t.Playing {
return nil, false
}
t.playRowPatMutex.Lock()
defer t.playRowPatMutex.Unlock()
t.PlayRow++
if t.PlayRow >= t.song.PatternRows() {
t.PlayRow = 0
t.PlayPattern++
}
if t.PlayPattern >= t.song.SequenceLength() {
t.PlayPattern = 0
}
if t.NoteTracking {
t.DisplayPattern = t.PlayPattern
t.CursorRow = t.PlayRow
}
notes := make([]Note, 0, 32)
for track := range t.song.Tracks {
patternIndex := t.song.Tracks[track].Sequence[t.PlayPattern]
note := t.song.Tracks[track].Patterns[patternIndex][t.PlayRow]
if note == 1 { // anything but hold causes an action.
continue
}
notes = append(notes, Note{curVoices[track], 0})
if note > 1 {
curVoices[track]++
first := t.song.FirstTrackVoice(track)
if curVoices[track] >= first+t.song.Tracks[track].NumVoices {
curVoices[track] = first
}
notes = append(notes, Note{curVoices[track], note})
}
}
t.ticked <- struct{}{}
return notes, true
})
buffer := make([]float32, 8192)
for {
select {
case <-closer:
return
default:
sequencer.ReadAudio(buffer)
output.WriteAudio(buffer)
}
}
}
func New(audioContext sointu.AudioContext) *Tracker {
t := &Tracker{
QuitButton: new(widget.Clickable),
CurrentOctave: 4,
audioContext: audioContext,
setPlaying: make(chan bool),
rowJump: make(chan int),
patternJump: make(chan int),
ticked: make(chan struct{}),
closer: make(chan struct{}),
}
go t.sequencerLoop(t.closer)
if err := t.LoadSong(defaultSong); err != nil {
panic(fmt.Errorf("cannot load default song: %w", err))
}
return t
}