sointu/tracker/player.go
vsariola adcf3ebce8 feat(sointu, tracker,...): restructure domain & tracker models
send targets are now by ID and Song has "Score" part, which is the notes for it. also, moved the model part separate of the actual gioui dependend stuff.

sorry to my future self about the code bomb; ended up too far and did not find an easy way to rewrite the history to make the steps smaller, so in the end, just squashed everything.
2021-02-28 14:24:54 +02:00

275 lines
6.7 KiB
Go

package tracker
import (
"math"
"sync"
"sync/atomic"
"github.com/vsariola/sointu"
)
type Player struct {
packedPos uint64
playCmds chan uint64
mutex sync.Mutex
runningID uint32
voiceNoteID []uint32
voiceReleased []bool
synth sointu.Synth
patch sointu.Patch
synthNotNil int32
}
type voiceNote struct {
voice int
note byte
}
// Position returns the current play position (song row), and a bool indicating
// if the player is currently playing. The function is threadsafe.
func (p *Player) Position() (SongRow, bool) {
packedPos := atomic.LoadUint64(&p.packedPos)
if packedPos == math.MaxUint64 { // stopped
return SongRow{}, false
}
return unpackPosition(packedPos), true
}
func (p *Player) Playing() bool {
packedPos := atomic.LoadUint64(&p.packedPos)
if packedPos == math.MaxUint64 { // stopped
return false
}
return true
}
func (p *Player) Play(position SongRow) {
position.Row-- // we'll advance this very shortly
p.playCmds <- packPosition(position)
}
func (p *Player) Stop() {
p.playCmds <- math.MaxUint64
}
func (p *Player) Disable() {
p.mutex.Lock()
p.synth = nil
atomic.StoreInt32(&p.synthNotNil, 0)
p.mutex.Unlock()
}
func (p *Player) Enabled() bool {
return atomic.LoadInt32(&p.synthNotNil) == 1
}
func NewPlayer(service sointu.SynthService, closer <-chan struct{}, patchs <-chan sointu.Patch, scores <-chan sointu.Score, samplesPerRows <-chan int, posChanged chan<- struct{}, outputs ...chan<- []float32) *Player {
p := &Player{playCmds: make(chan uint64, 16)}
go func() {
var score sointu.Score
buffer := make([]float32, 2048)
buffer2 := make([]float32, 2048)
zeros := make([]float32, 2048)
rowTime := 0
samplesPerRow := math.MaxInt32
var trackIDs []uint32
atomic.StoreUint64(&p.packedPos, math.MaxUint64)
for {
select {
case <-closer:
for _, o := range outputs {
close(o)
}
return
case patch := <-patchs:
p.mutex.Lock()
p.patch = patch
if p.synth != nil {
err := p.synth.Update(patch)
if err != nil {
p.synth = nil
atomic.StoreInt32(&p.synthNotNil, 0)
}
} else {
s, err := service.Compile(patch)
if err == nil {
p.synth = s
atomic.StoreInt32(&p.synthNotNil, 1)
for i := 0; i < 32; i++ {
s.Release(i)
}
}
}
p.mutex.Unlock()
case score = <-scores:
if row, playing := p.Position(); playing {
atomic.StoreUint64(&p.packedPos, packPosition(row.Wrap(score)))
}
case samplesPerRow = <-samplesPerRows:
case packedPos := <-p.playCmds:
atomic.StoreUint64(&p.packedPos, packedPos)
if packedPos == math.MaxUint64 {
p.mutex.Lock()
for _, id := range trackIDs {
p.release(id)
}
p.mutex.Unlock()
}
rowTime = math.MaxInt32
default:
row, playing := p.Position()
if playing && rowTime >= samplesPerRow && score.Length > 0 && score.RowsPerPattern > 0 {
row.Row++ // advance row (this is why we subtracted one in Play())
row = row.Wrap(score)
atomic.StoreUint64(&p.packedPos, packPosition(row))
select {
case posChanged <- struct{}{}:
default:
}
p.mutex.Lock()
lastVoice := 0
for i, t := range score.Tracks {
start := lastVoice
lastVoice = start + t.NumVoices
if row.Pattern < 0 || row.Pattern >= len(t.Order) {
continue
}
o := t.Order[row.Pattern]
if o < 0 || o >= len(t.Patterns) {
continue
}
pat := t.Patterns[o]
if row.Row < 0 || row.Row >= len(pat) {
continue
}
n := pat[row.Row]
for len(trackIDs) <= i {
trackIDs = append(trackIDs, 0)
}
if n != 1 && trackIDs[i] > 0 {
p.release(trackIDs[i])
}
if n > 1 && p.synth != nil {
trackIDs[i] = p.trigger(start, lastVoice, n)
}
}
p.mutex.Unlock()
rowTime = 0
}
if p.synth != nil {
renderTime := samplesPerRow - rowTime
if !playing {
renderTime = math.MaxInt32
}
p.mutex.Lock()
rendered, timeAdvanced, err := p.synth.Render(buffer, renderTime)
if err != nil {
p.synth = nil
atomic.StoreInt32(&p.synthNotNil, 0)
}
p.mutex.Unlock()
rowTime += timeAdvanced
for _, o := range outputs {
o <- buffer[:rendered*2]
}
buffer2, buffer = buffer, buffer2
} else {
rowTime += len(zeros) / 2
for _, o := range outputs {
o <- zeros
}
}
}
}
}()
return p
}
// Trigger is used to manually play a note on the sequencer when jamming. It is
// thread-safe. It starts to play one of the voice in the range voiceStart
// (inclusive) and voiceEnd (exclusive). It returns a id that can be called to
// release the voice playing the note (in case the voice has not been captured
// by someone else already).
func (p *Player) Trigger(voiceStart, voiceEnd int, note byte) uint32 {
if note <= 1 {
return 0
}
p.mutex.Lock()
id := p.trigger(voiceStart, voiceEnd, note)
p.mutex.Unlock()
return id
}
// Release is used to manually release a note on the player when jamming.
// Expects an ID that was previously acquired by calling Trigger.
func (p *Player) Release(ID uint32) {
if ID == 0 {
return
}
p.mutex.Lock()
p.release(ID)
p.mutex.Unlock()
}
func (p *Player) trigger(voiceStart, voiceEnd int, note byte) uint32 {
if p.synth == nil {
return 0
}
var oldestID uint32 = math.MaxUint32
p.runningID++
newID := p.runningID
oldestReleased := false
oldestVoice := 0
for i := voiceStart; i < voiceEnd; i++ {
for len(p.voiceReleased) <= i {
p.voiceReleased = append(p.voiceReleased, true)
}
for len(p.voiceNoteID) <= i {
p.voiceNoteID = append(p.voiceNoteID, 0)
}
// find a suitable voice to trigger. if the voice has been released,
// then we prefer to trigger that over a voice that is still playing. in
// case two voices are both playing or or both are released, we prefer
// the older one
id := p.voiceNoteID[i]
isReleased := p.voiceReleased[i]
if id < oldestID && (oldestReleased == isReleased) || (!oldestReleased && isReleased) {
oldestVoice = i
oldestID = id
oldestReleased = isReleased
}
}
p.voiceNoteID[oldestVoice] = newID
p.voiceReleased[oldestVoice] = false
if p.synth != nil {
p.synth.Trigger(oldestVoice, note)
}
return newID
}
func (p *Player) release(ID uint32) {
if p.synth == nil {
return
}
for i := 0; i < len(p.voiceNoteID); i++ {
if p.voiceNoteID[i] == ID && !p.voiceReleased[i] {
p.voiceReleased[i] = true
p.synth.Release(i)
return
}
}
}
func packPosition(pos SongRow) uint64 {
return (uint64(uint32(pos.Pattern)) << 32) + uint64(uint32(pos.Row))
}
func unpackPosition(packedPos uint64) SongRow {
pattern := int(int32(packedPos >> 32))
row := int(int32(packedPos & 0xFFFFFFFF))
return SongRow{Pattern: pattern, Row: row}
}