From 10f53bdbf7324bfb66767eb1a6c71bd30f898e1a Mon Sep 17 00:00:00 2001 From: vsariola <5684185+vsariola@users.noreply.github.com> Date: Thu, 11 Feb 2021 23:20:13 +0200 Subject: [PATCH] feat(sointu): rewrote sequencer to add support for keyjazzing --- sointu.go | 8 ++ tracker/keyevent.go | 42 ++++-- tracker/run.go | 5 +- tracker/sequencer.go | 310 +++++++++++++++++++++++++++++-------------- tracker/tracker.go | 60 +++------ 5 files changed, 272 insertions(+), 153 deletions(-) diff --git a/sointu.go b/sointu.go index 97fd9cc..b185878 100644 --- a/sointu.go +++ b/sointu.go @@ -304,6 +304,14 @@ func (s *Song) FirstTrackVoice(track int) int { 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 { ret := 0 for _, t := range s.Tracks { diff --git a/tracker/keyevent.go b/tracker/keyevent.go index 8073a45..7801c81 100644 --- a/tracker/keyevent.go +++ b/tracker/keyevent.go @@ -268,8 +268,15 @@ func (t *Tracker) KeyEvent(e key.Event) bool { return true } if val, ok := noteMap[e.Name]; ok { - t.NotePressed(val) - return true + 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 + } } } case EditUnits: @@ -279,10 +286,30 @@ func (t *Tracker) KeyEvent(e key.Event) bool { } if val, ok := unitKeyMap[name]; ok { if e.Modifiers.Contain(key.ModCtrl) { - t.AddUnit() + t.SetUnit(val) + return true } - t.SetUnit(val) - 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) } } } @@ -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] } -// 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 func (t *Tracker) NumberPressed(iv byte) { val := t.getCurrent() diff --git a/tracker/run.go b/tracker/run.go index cfb547e..01f4a96 100644 --- a/tracker/run.go +++ b/tracker/run.go @@ -1,19 +1,20 @@ package tracker import ( + "os" + "gioui.org/app" "gioui.org/io/key" "gioui.org/io/system" "gioui.org/layout" "gioui.org/op" - "os" ) func (t *Tracker) Run(w *app.Window) error { var ops op.Ops for { select { - case <-t.ticked: + case <-t.refresh: w.Invalidate() case e := <-w.Events(): switch e := e.(type) { diff --git a/tracker/sequencer.go b/tracker/sequencer.go index 507873b..564775c 100644 --- a/tracker/sequencer.go +++ b/tracker/sequencer.go @@ -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) } } } diff --git a/tracker/tracker.go b/tracker/tracker.go index 95bc169..92cfd1d 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -77,9 +77,10 @@ type Tracker struct { BottomHorizontalSplit *Split VerticalSplit *Split StackUse []int + KeyPlaying map[string]func() sequencer *Sequencer - ticked chan struct{} + refresh chan struct{} setPlaying chan bool rowJump 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 { newOctave := t.Octave.Value + delta if newOctave < 0 { @@ -557,7 +539,7 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr setPlaying: make(chan bool), rowJump: 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{}), undoStack: []sointu.Song{}, redoStack: []sointu.Song{}, @@ -567,6 +549,7 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr BottomHorizontalSplit: new(Split), VerticalSplit: new(Split), ChooseUnitTypeList: &layout.List{Axis: layout.Vertical}, + KeyPlaying: make(map[string]func()), } t.UnitDragList.HoverItem = -1 t.InstrumentDragList.HoverItem = -1 @@ -578,12 +561,11 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr for range allUnits { t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable)) } - curVoices := make([]int, 32) - t.sequencer = NewSequencer(synthService, func() ([]Note, bool) { + t.sequencer = NewSequencer(2048, synthService, audioContext, func(row []RowNote) []RowNote { t.playRowPatMutex.Lock() if !t.Playing { t.playRowPatMutex.Unlock() - return nil, false + return nil } t.PlayPosition.Row++ t.PlayPosition.Wrap(t.song) @@ -591,28 +573,20 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr t.Cursor.SongRow = t.PlayPosition t.SelectionCorner.SongRow = t.PlayPosition } - notes := make([]Note, 0, 32) - for track := range t.song.Tracks { - patternIndex := t.song.Tracks[track].Sequence[t.PlayPosition.Pattern] - note := t.song.Tracks[track].Patterns[patternIndex][t.PlayPosition.Row] - 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}) - } + for _, track := range t.song.Tracks { + patternIndex := track.Sequence[t.PlayPosition.Pattern] + note := track.Patterns[patternIndex][t.PlayPosition.Row] + row = append(row, RowNote{Note: note, NumVoices: track.NumVoices}) } t.playRowPatMutex.Unlock() - t.ticked <- struct{}{} - return notes, true + select { + 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 { panic(fmt.Errorf("cannot load default song: %w", err)) }