mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
139 lines
3.5 KiB
Go
139 lines
3.5 KiB
Go
package tracker
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"sync"
|
|
|
|
"github.com/vsariola/sointu"
|
|
)
|
|
|
|
// 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
|
|
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.
|
|
iterator func() ([]Note, bool)
|
|
rowTime int
|
|
rowLength int
|
|
}
|
|
|
|
type Note struct {
|
|
Voice int
|
|
Note byte
|
|
}
|
|
|
|
func NewSequencer(service sointu.SynthService, iterator func() ([]Note, bool)) *Sequencer {
|
|
return &Sequencer{
|
|
service: service,
|
|
iterator: iterator,
|
|
rowLength: math.MaxInt32,
|
|
rowTime: math.MaxInt32,
|
|
}
|
|
}
|
|
|
|
func (s *Sequencer) ReadAudio(buffer []float32) (int, error) {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
totalRendered := 0
|
|
for i := 0; i < SEQUENCER_MAX_READ_TRIES; i++ {
|
|
gotRow := true
|
|
if s.rowTime >= s.rowLength {
|
|
var row []Note
|
|
s.mutex.Unlock()
|
|
row, gotRow = s.iterator()
|
|
s.mutex.Lock()
|
|
if gotRow {
|
|
for _, n := range row {
|
|
s.doNote(n.Voice, n.Note)
|
|
}
|
|
s.rowTime = 0
|
|
} else {
|
|
for i := 0; i < 32; i++ {
|
|
s.doNote(i, 0)
|
|
}
|
|
}
|
|
}
|
|
rowTimeRemaining := s.rowLength - s.rowTime
|
|
if !gotRow {
|
|
rowTimeRemaining = math.MaxInt32
|
|
}
|
|
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
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
|
|
// Updates the patch of the synth
|
|
func (s *Sequencer) SetPatch(patch sointu.Patch) {
|
|
s.mutex.Lock()
|
|
if s.synth != nil {
|
|
s.synth.Update(patch)
|
|
} else {
|
|
s.synth, _ = s.service.Compile(patch)
|
|
}
|
|
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 s.synth != nil {
|
|
if note == 0 {
|
|
s.synth.Release(voice)
|
|
} else {
|
|
s.synth.Trigger(voice, note)
|
|
}
|
|
}
|
|
}
|