mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
feat(sointu): rewrote sequencer to add support for keyjazzing
This commit is contained in:
parent
b9c8218ca4
commit
10f53bdbf7
@ -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 {
|
||||||
|
@ -268,8 +268,15 @@ 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 {
|
||||||
return true
|
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:
|
case EditUnits:
|
||||||
@ -279,10 +286,30 @@ 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)
|
||||||
|
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]
|
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()
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
validSynth int32
|
||||||
// that the synth is not changed when audio is being read
|
closer chan struct{}
|
||||||
mutex sync.Mutex
|
setPatch chan sointu.Patch
|
||||||
synth sointu.Synth
|
setRowLength chan int
|
||||||
validSynth int32
|
noteOn chan noteOnEvent
|
||||||
service sointu.SynthService
|
noteOff chan uint32
|
||||||
// this iterator is a bit unconventional in the sense that it might return
|
synth sointu.Synth
|
||||||
// hasNext false, but might still return hasNext true in future attempts if
|
voiceNoteID []uint32
|
||||||
// new rows become available.
|
voiceReleased []bool
|
||||||
iterator func() ([]Note, bool)
|
idCounter uint32
|
||||||
rowTime int
|
|
||||||
rowLength int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
}
|
||||||
|
|
||||||
|
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) {
|
func (s *Sequencer) loop(bufferSize int, service sointu.SynthService, context sointu.AudioContext, iterator func([]RowNote) []RowNote) {
|
||||||
s.mutex.Lock()
|
buffer := make([]float32, bufferSize)
|
||||||
defer s.mutex.Unlock()
|
renderTries := 0
|
||||||
totalRendered := 0
|
audioOut := context.Output()
|
||||||
for i := 0; i < SEQUENCER_MAX_READ_TRIES; i++ {
|
defer audioOut.Close()
|
||||||
gotRow := true
|
rowIn := make([]RowNote, 32)
|
||||||
if s.rowTime >= s.rowLength {
|
rowLength := math.MaxInt32
|
||||||
var row []Note
|
rowTimeRemaining := 0
|
||||||
s.mutex.Unlock()
|
trackNotes := make([]uint32, 32)
|
||||||
row, gotRow = s.iterator()
|
for {
|
||||||
s.mutex.Lock()
|
for !s.Enabled() {
|
||||||
if gotRow {
|
select {
|
||||||
for _, n := range row {
|
case <-s.closer:
|
||||||
s.doNote(n.Voice, n.Note)
|
return
|
||||||
}
|
case <-s.noteOn:
|
||||||
s.rowTime = 0
|
case <-s.noteOff:
|
||||||
} else {
|
case rowLength = <-s.setRowLength:
|
||||||
for i := 0; i < 32; i++ {
|
case patch := <-s.setPatch:
|
||||||
s.doNote(i, 0)
|
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:
|
||||||
if err != nil {
|
s.trigger(n.voiceStart, n.voiceEnd, n.note, n.id)
|
||||||
s.Disable()
|
case n := <-s.noteOff:
|
||||||
}
|
s.release(n)
|
||||||
totalRendered += rendered
|
case rowLength = <-s.setRowLength:
|
||||||
s.rowTime += timeAdvanced
|
case patch := <-s.setPatch:
|
||||||
} else {
|
err := s.synth.Update(patch)
|
||||||
for totalRendered*2 < len(buffer) && rowTimeRemaining > 0 {
|
if err != nil {
|
||||||
buffer[totalRendered*2] = 0
|
s.Disable()
|
||||||
buffer[totalRendered*2+1] = 0
|
break
|
||||||
totalRendered++
|
}
|
||||||
s.rowTime++
|
default:
|
||||||
rowTimeRemaining--
|
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 {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user