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

@ -304,6 +304,14 @@ func (s *Song) FirstTrackVoice(track int) int {
return ret return ret
} }
func (s *Song) FirstInstrumentVoice(instrument int) int {
ret := 0
for _, i := range s.Patch.Instruments[:instrument] {
ret += i.NumVoices
}
return ret
}
func (s *Song) TotalTrackVoices() int { func (s *Song) TotalTrackVoices() int {
ret := 0 ret := 0
for _, t := range s.Tracks { for _, t := range s.Tracks {

View File

@ -268,10 +268,17 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
return true return true
} }
if val, ok := noteMap[e.Name]; ok { if val, ok := noteMap[e.Name]; ok {
t.NotePressed(val) if _, ok := t.KeyPlaying[e.Name]; !ok {
n := getNoteValue(int(t.Octave.Value), val)
t.SetCurrentNote(n)
trk := t.Cursor.Track
start := t.song.FirstTrackVoice(trk)
end := start + t.song.Tracks[trk].NumVoices
t.KeyPlaying[e.Name] = t.sequencer.Trigger(start, end, n)
return true return true
} }
} }
}
case EditUnits: case EditUnits:
name := e.Name name := e.Name
if !e.Modifiers.Contain(key.ModShift) { if !e.Modifiers.Contain(key.ModShift) {
@ -279,12 +286,32 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
} }
if val, ok := unitKeyMap[name]; ok { if val, ok := unitKeyMap[name]; ok {
if e.Modifiers.Contain(key.ModCtrl) { if e.Modifiers.Contain(key.ModCtrl) {
t.AddUnit()
}
t.SetUnit(val) t.SetUnit(val)
return true return true
} }
} }
fallthrough
case EditParameters:
if val, ok := noteMap[e.Name]; ok {
if _, ok := t.KeyPlaying[e.Name]; !ok {
note := getNoteValue(int(t.Octave.Value), val)
instr := t.CurrentInstrument
start := t.song.FirstInstrumentVoice(instr)
end := start + t.song.Patch.Instruments[instr].NumVoices
t.KeyPlaying[e.Name] = t.sequencer.Trigger(start, end, note)
return false
}
}
}
}
if e.State == key.Release {
if f, ok := t.KeyPlaying[e.Name]; ok {
f()
delete(t.KeyPlaying, e.Name)
if t.EditMode == EditTracks && t.Playing && t.getCurrent() == 1 {
t.SetCurrentNote(0)
}
}
} }
return false return false
} }
@ -294,11 +321,6 @@ func (t *Tracker) getCurrent() byte {
return t.song.Tracks[t.Cursor.Track].Patterns[t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern]][t.Cursor.Row] return t.song.Tracks[t.Cursor.Track].Patterns[t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern]][t.Cursor.Row]
} }
// NotePressed handles incoming key presses while in the note column
func (t *Tracker) NotePressed(val int) {
t.SetCurrentNote(getNoteValue(int(t.Octave.Value), val))
}
// NumberPressed handles incoming presses while in either of the hex number columns // NumberPressed handles incoming presses while in either of the hex number columns
func (t *Tracker) NumberPressed(iv byte) { func (t *Tracker) NumberPressed(iv byte) {
val := t.getCurrent() val := t.getCurrent()

View File

@ -1,19 +1,20 @@
package tracker package tracker
import ( import (
"os"
"gioui.org/app" "gioui.org/app"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"os"
) )
func (t *Tracker) Run(w *app.Window) error { func (t *Tracker) Run(w *app.Window) error {
var ops op.Ops var ops op.Ops
for { for {
select { select {
case <-t.ticked: case <-t.refresh:
w.Invalidate() w.Invalidate()
case e := <-w.Events(): case e := <-w.Events():
switch e := e.(type) { switch e := e.(type) {

View File

@ -1,9 +1,7 @@
package tracker package tracker
import ( import (
"fmt"
"math" "math"
"sync"
"sync/atomic" "sync/atomic"
"github.com/vsariola/sointu" "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 // iterator. Note that the iterator should be thread safe, as the ReadAudio
// might be called from another go routine. // might be called from another go routine.
type Sequencer struct { 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 validSynth int32
service sointu.SynthService closer chan struct{}
// this iterator is a bit unconventional in the sense that it might return setPatch chan sointu.Patch
// hasNext false, but might still return hasNext true in future attempts if setRowLength chan int
// new rows become available. noteOn chan noteOnEvent
iterator func() ([]Note, bool) noteOff chan uint32
rowTime int synth sointu.Synth
rowLength int voiceNoteID []uint32
voiceReleased []bool
idCounter uint32
} }
type Note struct { type RowNote struct {
Voice int NumVoices int
Note byte Note byte
} }
func NewSequencer(service sointu.SynthService, iterator func() ([]Note, bool)) *Sequencer { type noteOnEvent struct {
return &Sequencer{ voiceStart int
service: service, voiceEnd int
iterator: iterator, note byte
rowLength: math.MaxInt32, id uint32
rowTime: math.MaxInt32,
}
} }
func (s *Sequencer) ReadAudio(buffer []float32) (int, error) { type noteID struct {
s.mutex.Lock() voice int
defer s.mutex.Unlock() id uint32
totalRendered := 0 }
for i := 0; i < SEQUENCER_MAX_READ_TRIES; i++ {
gotRow := true func NewSequencer(bufferSize int, service sointu.SynthService, context sointu.AudioContext, iterator func([]RowNote) []RowNote) *Sequencer {
if s.rowTime >= s.rowLength { ret := &Sequencer{
var row []Note closer: make(chan struct{}),
s.mutex.Unlock() setPatch: make(chan sointu.Patch, 32),
row, gotRow = s.iterator() setRowLength: make(chan int, 32),
s.mutex.Lock() noteOn: make(chan noteOnEvent, 32),
if gotRow { noteOff: make(chan uint32, 32),
for _, n := range row { voiceNoteID: make([]uint32, 32),
s.doNote(n.Voice, n.Note) voiceReleased: make([]bool, 32),
} }
s.rowTime = 0 // the iterator is a bit unconventional in the sense that it might return
} else { // false to indicate that there is no row available, but might still return
for i := 0; i < 32; i++ { // true in future attempts if new rows become available.
s.doNote(i, 0) go ret.loop(bufferSize, service, context, iterator)
return ret
}
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 released := false
if !gotRow { for s.Enabled() {
rowTimeRemaining = math.MaxInt32 select {
} case <-s.closer:
if s.Enabled() { return
rendered, timeAdvanced, err := s.synth.Render(buffer[totalRendered*2:], rowTimeRemaining) 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 { if err != nil {
s.Disable() s.Disable()
break
} }
totalRendered += rendered default:
s.rowTime += timeAdvanced 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 { } else {
for totalRendered*2 < len(buffer) && rowTimeRemaining > 0 { if !released {
buffer[totalRendered*2] = 0 s.releaseVoiceRange(0, len(s.voiceNoteID))
buffer[totalRendered*2+1] = 0 released = true
totalRendered++ }
s.rowTime++ rowTimeRemaining = 0
rowTimeRemaining-- renderTime = math.MaxInt32
} }
} }
if totalRendered*2 >= len(buffer) { rendered, timeAdvanced, err := s.synth.Render(buffer, renderTime)
return totalRendered * 2, nil if err != nil {
s.Disable()
break
} }
} rowTimeRemaining -= timeAdvanced
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) if timeAdvanced == 0 {
} renderTries++
// 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 { } else {
s.synth, err = s.service.Compile(patch) renderTries = 0
}
if renderTries >= SEQUENCER_MAX_READ_TRIES {
s.Disable()
break
}
err = audioOut.WriteAudio(buffer[:2*rendered])
if err != nil {
s.Disable()
break
}
}
} }
if err == nil {
atomic.StoreInt32(&s.validSynth, 1)
} }
s.mutex.Unlock()
} }
func (s *Sequencer) Enabled() bool { func (s *Sequencer) Enabled() bool {
@ -121,32 +170,97 @@ func (s *Sequencer) Disable() {
} }
func (s *Sequencer) SetRowLength(rowLength int) { func (s *Sequencer) SetRowLength(rowLength int) {
s.mutex.Lock() s.setRowLength <- rowLength
s.rowLength = rowLength }
s.mutex.Unlock()
// 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 // Trigger is used to manually play a note on the sequencer when jamming. It is
// thread-safe. // thread-safe. It starts to play one of the voice in the range voiceStart
func (s *Sequencer) Trigger(voice int, note byte) { // (inclusive) and voiceEnd (exclusive). It returns a release function that can
s.mutex.Lock() // be called to release the voice playing the note (in case the voice has not
s.doNote(voice, note) // been captured by someone else already). Note that Trigger will never block,
s.mutex.Unlock() // 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 func (s *Sequencer) getNewID() uint32 {
// is thread-safe. return atomic.AddUint32(&s.idCounter, 1)
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) enable() {
func (s *Sequencer) doNote(voice int, note byte) { atomic.StoreInt32(&s.validSynth, 1)
if s.synth != nil { }
if note == 0 {
s.synth.Release(voice) func (s *Sequencer) trigger(voiceStart, voiceEnd int, note byte, newID uint32) {
} else { if !s.Enabled() {
s.synth.Trigger(voice, note) 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)
} }
} }
} }

View File

@ -77,9 +77,10 @@ type Tracker struct {
BottomHorizontalSplit *Split BottomHorizontalSplit *Split
VerticalSplit *Split VerticalSplit *Split
StackUse []int StackUse []int
KeyPlaying map[string]func()
sequencer *Sequencer sequencer *Sequencer
ticked chan struct{} refresh chan struct{}
setPlaying chan bool setPlaying chan bool
rowJump chan int rowJump chan int
patternJump chan int patternJump chan int
@ -131,25 +132,6 @@ func (t *Tracker) TogglePlay() {
} }
} }
func (t *Tracker) sequencerLoop(closer <-chan struct{}) {
output := t.audioContext.Output()
defer output.Close()
buffer := make([]float32, 8192)
for {
select {
case <-closer:
return
default:
read, _ := t.sequencer.ReadAudio(buffer)
for read < len(buffer) {
buffer[read] = 0
read++
}
output.WriteAudio(buffer)
}
}
}
func (t *Tracker) ChangeOctave(delta int) bool { func (t *Tracker) ChangeOctave(delta int) bool {
newOctave := t.Octave.Value + delta newOctave := t.Octave.Value + delta
if newOctave < 0 { if newOctave < 0 {
@ -557,7 +539,7 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr
setPlaying: make(chan bool), setPlaying: make(chan bool),
rowJump: make(chan int), rowJump: make(chan int),
patternJump: make(chan int), patternJump: make(chan int),
ticked: make(chan struct{}), refresh: make(chan struct{}, 1), // use non-blocking sends; no need to queue extra ticks if one is queued already
closer: make(chan struct{}), closer: make(chan struct{}),
undoStack: []sointu.Song{}, undoStack: []sointu.Song{},
redoStack: []sointu.Song{}, redoStack: []sointu.Song{},
@ -567,6 +549,7 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr
BottomHorizontalSplit: new(Split), BottomHorizontalSplit: new(Split),
VerticalSplit: new(Split), VerticalSplit: new(Split),
ChooseUnitTypeList: &layout.List{Axis: layout.Vertical}, ChooseUnitTypeList: &layout.List{Axis: layout.Vertical},
KeyPlaying: make(map[string]func()),
} }
t.UnitDragList.HoverItem = -1 t.UnitDragList.HoverItem = -1
t.InstrumentDragList.HoverItem = -1 t.InstrumentDragList.HoverItem = -1
@ -578,12 +561,11 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr
for range allUnits { for range allUnits {
t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable)) t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable))
} }
curVoices := make([]int, 32) t.sequencer = NewSequencer(2048, synthService, audioContext, func(row []RowNote) []RowNote {
t.sequencer = NewSequencer(synthService, func() ([]Note, bool) {
t.playRowPatMutex.Lock() t.playRowPatMutex.Lock()
if !t.Playing { if !t.Playing {
t.playRowPatMutex.Unlock() t.playRowPatMutex.Unlock()
return nil, false return nil
} }
t.PlayPosition.Row++ t.PlayPosition.Row++
t.PlayPosition.Wrap(t.song) t.PlayPosition.Wrap(t.song)
@ -591,28 +573,20 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr
t.Cursor.SongRow = t.PlayPosition t.Cursor.SongRow = t.PlayPosition
t.SelectionCorner.SongRow = t.PlayPosition t.SelectionCorner.SongRow = t.PlayPosition
} }
notes := make([]Note, 0, 32) for _, track := range t.song.Tracks {
for track := range t.song.Tracks { patternIndex := track.Sequence[t.PlayPosition.Pattern]
patternIndex := t.song.Tracks[track].Sequence[t.PlayPosition.Pattern] note := track.Patterns[patternIndex][t.PlayPosition.Row]
note := t.song.Tracks[track].Patterns[patternIndex][t.PlayPosition.Row] row = append(row, RowNote{Note: note, NumVoices: track.NumVoices})
if note == 1 { // anything but hold causes an action.
continue
}
first := t.song.FirstTrackVoice(track)
notes = append(notes, Note{first + curVoices[track], 0})
if note > 1 {
curVoices[track]++
if curVoices[track] >= t.song.Tracks[track].NumVoices {
curVoices[track] = 0
}
notes = append(notes, Note{first + curVoices[track], note})
}
} }
t.playRowPatMutex.Unlock() t.playRowPatMutex.Unlock()
t.ticked <- struct{}{} select {
return notes, true case t.refresh <- struct{}{}:
default:
// message dropped, there's already a tick queued, so no need to queue extra
}
return row
}) })
go t.sequencerLoop(t.closer)
if err := t.LoadSong(defaultSong.Copy()); err != nil { if err := t.LoadSong(defaultSong.Copy()); err != nil {
panic(fmt.Errorf("cannot load default song: %w", err)) panic(fmt.Errorf("cannot load default song: %w", err))
} }