feat(sointu): rewrote sequencer to add support for keyjazzing

This commit is contained in:
vsariola
2021-02-11 23:20:13 +02:00
parent b9c8218ca4
commit 10f53bdbf7
5 changed files with 272 additions and 153 deletions

View File

@ -1,9 +1,7 @@
package tracker
import (
"fmt"
"math"
"sync"
"sync/atomic"
"github.com/vsariola/sointu"
@ -20,96 +18,147 @@ const SEQUENCER_MAX_READ_TRIES = 1000
// 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
validSynth int32
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
validSynth int32
closer chan struct{}
setPatch chan sointu.Patch
setRowLength chan int
noteOn chan noteOnEvent
noteOff chan uint32
synth sointu.Synth
voiceNoteID []uint32
voiceReleased []bool
idCounter uint32
}
type Note struct {
Voice int
Note byte
type RowNote struct {
NumVoices 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,
type noteOnEvent struct {
voiceStart int
voiceEnd int
note byte
id uint32
}
type noteID struct {
voice int
id uint32
}
func NewSequencer(bufferSize int, service sointu.SynthService, context sointu.AudioContext, iterator func([]RowNote) []RowNote) *Sequencer {
ret := &Sequencer{
closer: make(chan struct{}),
setPatch: make(chan sointu.Patch, 32),
setRowLength: make(chan int, 32),
noteOn: make(chan noteOnEvent, 32),
noteOff: make(chan uint32, 32),
voiceNoteID: make([]uint32, 32),
voiceReleased: make([]bool, 32),
}
// the iterator is a bit unconventional in the sense that it might return
// false to indicate that there is no row available, but might still return
// true in future attempts if new rows become available.
go ret.loop(bufferSize, service, context, iterator)
return ret
}
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)
func (s *Sequencer) loop(bufferSize int, service sointu.SynthService, context sointu.AudioContext, iterator func([]RowNote) []RowNote) {
buffer := make([]float32, bufferSize)
renderTries := 0
audioOut := context.Output()
defer audioOut.Close()
rowIn := make([]RowNote, 32)
rowLength := math.MaxInt32
rowTimeRemaining := 0
trackNotes := make([]uint32, 32)
for {
for !s.Enabled() {
select {
case <-s.closer:
return
case <-s.noteOn:
case <-s.noteOff:
case rowLength = <-s.setRowLength:
case patch := <-s.setPatch:
var err error
s.synth, err = service.Compile(patch)
if err == nil {
s.enable()
break
}
}
}
rowTimeRemaining := s.rowLength - s.rowTime
if !gotRow {
rowTimeRemaining = math.MaxInt32
}
if s.Enabled() {
rendered, timeAdvanced, err := s.synth.Render(buffer[totalRendered*2:], rowTimeRemaining)
if err != nil {
s.Disable()
}
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--
released := false
for s.Enabled() {
select {
case <-s.closer:
return
case n := <-s.noteOn:
s.trigger(n.voiceStart, n.voiceEnd, n.note, n.id)
case n := <-s.noteOff:
s.release(n)
case rowLength = <-s.setRowLength:
case patch := <-s.setPatch:
err := s.synth.Update(patch)
if err != nil {
s.Disable()
break
}
default:
renderTime := rowTimeRemaining
if rowTimeRemaining <= 0 {
rowOut := iterator(rowIn[:0])
if len(rowOut) > 0 {
curVoice := 0
for i, rn := range rowOut {
end := curVoice + rn.NumVoices
if rn.Note != 1 {
s.release(trackNotes[i])
}
if rn.Note > 1 {
id := s.getNewID()
s.trigger(curVoice, end, rn.Note, id)
trackNotes[i] = id
}
curVoice = end
}
rowTimeRemaining = rowLength
renderTime = rowLength
released = false
} else {
if !released {
s.releaseVoiceRange(0, len(s.voiceNoteID))
released = true
}
rowTimeRemaining = 0
renderTime = math.MaxInt32
}
}
rendered, timeAdvanced, err := s.synth.Render(buffer, renderTime)
if err != nil {
s.Disable()
break
}
rowTimeRemaining -= timeAdvanced
if timeAdvanced == 0 {
renderTries++
} else {
renderTries = 0
}
if renderTries >= SEQUENCER_MAX_READ_TRIES {
s.Disable()
break
}
err = audioOut.WriteAudio(buffer[:2*rendered])
if err != nil {
s.Disable()
break
}
}
}
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()
var err error
if s.Enabled() {
err = s.synth.Update(patch)
} else {
s.synth, err = s.service.Compile(patch)
}
if err == nil {
atomic.StoreInt32(&s.validSynth, 1)
}
s.mutex.Unlock()
}
func (s *Sequencer) Enabled() bool {
@ -121,32 +170,97 @@ func (s *Sequencer) Disable() {
}
func (s *Sequencer) SetRowLength(rowLength int) {
s.mutex.Lock()
s.rowLength = rowLength
s.mutex.Unlock()
s.setRowLength <- rowLength
}
// Close closes the sequencer and releases all its resources
func (s *Sequencer) Close() {
s.closer <- struct{}{}
}
// SetPatch updates the synth to match given patch
func (s *Sequencer) SetPatch(patch sointu.Patch) {
s.setPatch <- patch.Copy()
}
// 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()
// thread-safe. It starts to play one of the voice in the range voiceStart
// (inclusive) and voiceEnd (exclusive). It returns a release function that can
// be called to release the voice playing the note (in case the voice has not
// been captured by someone else already). Note that Trigger will never block,
// but calling the release function might block until the sequencer has been
// able to assign a voice to the note.
func (s *Sequencer) Trigger(voiceStart, voiceEnd int, note byte) func() {
if note <= 1 {
return func() {}
}
id := s.getNewID()
e := noteOnEvent{
voiceStart: voiceStart,
voiceEnd: voiceEnd,
note: note,
id: id,
}
s.noteOn <- e
return func() {
s.noteOff <- id // now, tell the sequencer to stop it
}
}
// 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)
func (s *Sequencer) getNewID() uint32 {
return atomic.AddUint32(&s.idCounter, 1)
}
// 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)
func (s *Sequencer) enable() {
atomic.StoreInt32(&s.validSynth, 1)
}
func (s *Sequencer) trigger(voiceStart, voiceEnd int, note byte, newID uint32) {
if !s.Enabled() {
return
}
var oldestID uint32 = math.MaxUint32
oldestReleased := false
oldestVoice := 0
for i := voiceStart; i < voiceEnd; i++ {
// 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 := s.voiceNoteID[i]
isReleased := s.voiceReleased[i]
if id < oldestID && (oldestReleased == isReleased) || (!oldestReleased && isReleased) {
oldestVoice = i
oldestID = id
oldestReleased = isReleased
}
}
s.voiceNoteID[oldestVoice] = newID
s.voiceReleased[oldestVoice] = false
s.synth.Trigger(oldestVoice, note)
}
func (s *Sequencer) release(id uint32) {
if !s.Enabled() {
return
}
for i := 0; i < len(s.voiceNoteID); i++ {
if s.voiceNoteID[i] == id && !s.voiceReleased[i] {
s.voiceReleased[i] = true
s.synth.Release(i)
return
}
}
}
func (s *Sequencer) releaseVoiceRange(voiceStart, voiceEnd int) {
if !s.Enabled() {
return
}
for i := voiceStart; i < voiceEnd; i++ {
if !s.voiceReleased[i] {
s.voiceReleased[i] = true
s.synth.Release(i)
}
}
}