mirror of
https://github.com/vsariola/sointu.git
synced 2026-06-01 03:49:08 -04:00
refactor(tracker): group Model methods, with each group in one source file
This commit is contained in:
parent
b93304adab
commit
86ca3fb300
@@ -8,7 +8,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
"time"
|
|
||||||
|
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
@@ -60,15 +59,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
model := tracker.NewModel(broker, cmd.Synthers, midiContext, recoveryFile)
|
model := tracker.NewModel(broker, cmd.Synthers, midiContext, recoveryFile)
|
||||||
player := tracker.NewPlayer(broker, cmd.Synthers[0])
|
player := tracker.NewPlayer(broker, cmd.Synthers[0])
|
||||||
detector := tracker.NewDetector(broker)
|
|
||||||
specan := tracker.NewSpecAnalyzer(broker)
|
|
||||||
go detector.Run()
|
|
||||||
go specan.Run()
|
|
||||||
|
|
||||||
if a := flag.Args(); len(a) > 0 {
|
if a := flag.Args(); len(a) > 0 {
|
||||||
f, err := os.Open(a[0])
|
f, err := os.Open(a[0])
|
||||||
if err == nil {
|
if err == nil {
|
||||||
model.ReadSong(f)
|
model.Song().Read(f)
|
||||||
}
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
@@ -82,10 +77,7 @@ func main() {
|
|||||||
go func() {
|
go func() {
|
||||||
trackerUi.Main()
|
trackerUi.Main()
|
||||||
audioCloser.Close()
|
audioCloser.Close()
|
||||||
tracker.TrySend(broker.CloseDetector, struct{}{})
|
model.Close()
|
||||||
tracker.TrySend(broker.CloseSpecAn, struct{}{})
|
|
||||||
tracker.TimeoutReceive(broker.FinishedDetector, 3*time.Second)
|
|
||||||
tracker.TimeoutReceive(broker.FinishedSpecAn, 3*time.Second)
|
|
||||||
if *cpuprofile != "" {
|
if *cpuprofile != "" {
|
||||||
pprof.StopCPUProfile()
|
pprof.StopCPUProfile()
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|||||||
@@ -48,18 +48,14 @@ func init() {
|
|||||||
broker := tracker.NewBroker()
|
broker := tracker.NewBroker()
|
||||||
model := tracker.NewModel(broker, cmd.Synthers, cmd.NewMidiContext(broker), recoveryFile)
|
model := tracker.NewModel(broker, cmd.Synthers, cmd.NewMidiContext(broker), recoveryFile)
|
||||||
player := tracker.NewPlayer(broker, cmd.Synthers[0])
|
player := tracker.NewPlayer(broker, cmd.Synthers[0])
|
||||||
detector := tracker.NewDetector(broker)
|
|
||||||
specan := tracker.NewSpecAnalyzer(broker)
|
|
||||||
go detector.Run()
|
|
||||||
go specan.Run()
|
|
||||||
|
|
||||||
t := gioui.NewTracker(model)
|
t := gioui.NewTracker(model)
|
||||||
model.InstrEnlarged().SetValue(true)
|
model.Play().TrackerHidden().SetValue(true)
|
||||||
// since the VST is usually working without any regard for the tracks
|
// since the VST is usually working without any regard for the tracks
|
||||||
// until recording, disable the Instrument-Track linking by default
|
// until recording, disable the Instrument-Track linking by default
|
||||||
// because it might just confuse the user why instrument cannot be
|
// because it might just confuse the user why instrument cannot be
|
||||||
// swapped/added etc.
|
// swapped/added etc.
|
||||||
model.LinkInstrTrack().SetValue(false)
|
model.Track().LinkInstrument().SetValue(false)
|
||||||
go t.Main()
|
go t.Main()
|
||||||
context := &VSTIProcessContext{host: h}
|
context := &VSTIProcessContext{host: h}
|
||||||
buf := make(sointu.AudioBuffer, 1024)
|
buf := make(sointu.AudioBuffer, 1024)
|
||||||
@@ -112,24 +108,21 @@ func init() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
CloseFunc: func() {
|
CloseFunc: func() {
|
||||||
tracker.TrySend(broker.CloseDetector, struct{}{})
|
|
||||||
tracker.TrySend(broker.CloseGUI, struct{}{})
|
tracker.TrySend(broker.CloseGUI, struct{}{})
|
||||||
tracker.TrySend(broker.CloseSpecAn, struct{}{})
|
model.Close()
|
||||||
tracker.TimeoutReceive(broker.FinishedDetector, 3*time.Second)
|
|
||||||
tracker.TimeoutReceive(broker.FinishedGUI, 3*time.Second)
|
tracker.TimeoutReceive(broker.FinishedGUI, 3*time.Second)
|
||||||
tracker.TimeoutReceive(broker.FinishedSpecAn, 3*time.Second)
|
|
||||||
},
|
},
|
||||||
GetChunkFunc: func(isPreset bool) []byte {
|
GetChunkFunc: func(isPreset bool) []byte {
|
||||||
retChn := make(chan []byte)
|
retChn := make(chan []byte)
|
||||||
|
|
||||||
if !tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { retChn <- t.MarshalRecovery() }}) {
|
if !tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { retChn <- t.History().MarshalRecovery() }}) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ret, _ := tracker.TimeoutReceive(retChn, 5*time.Second) // ret will be nil if timeout or channel closed
|
ret, _ := tracker.TimeoutReceive(retChn, 5*time.Second) // ret will be nil if timeout or channel closed
|
||||||
return ret
|
return ret
|
||||||
},
|
},
|
||||||
SetChunkFunc: func(data []byte, isPreset bool) {
|
SetChunkFunc: func(data []byte, isPreset bool) {
|
||||||
tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { t.UnmarshalRecovery(data) }})
|
tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { t.History().UnmarshalRecovery(data) }})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,675 +0,0 @@
|
|||||||
package tracker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
|
||||||
"github.com/vsariola/sointu/vm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// Action describes a user action that can be performed on the model, which
|
|
||||||
// can be initiated by calling the Do() method. It is usually initiated by a
|
|
||||||
// button press or a menu item. Action advertises whether it is enabled, so
|
|
||||||
// UI can e.g. gray out buttons when the underlying action is not allowed.
|
|
||||||
// The underlying Doer can optionally implement the Enabler interface to
|
|
||||||
// decide if the action is enabled or not; if it does not implement the
|
|
||||||
// Enabler interface, the action is always allowed.
|
|
||||||
Action struct {
|
|
||||||
doer Doer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Doer is an interface that defines a single Do() method, which is called
|
|
||||||
// when an action is performed.
|
|
||||||
Doer interface {
|
|
||||||
Do()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enabler is an interface that defines a single Enabled() method, which
|
|
||||||
// is used by the UI to check if UI Action/Bool/Int etc. is enabled or not.
|
|
||||||
Enabler interface {
|
|
||||||
Enabled() bool
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Action methods
|
|
||||||
|
|
||||||
func MakeAction(doer Doer) Action {
|
|
||||||
return Action{doer: doer}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Action) Do() {
|
|
||||||
e, ok := a.doer.(Enabler)
|
|
||||||
if ok && !e.Enabled() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if a.doer != nil {
|
|
||||||
a.doer.Do()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Action) Enabled() bool {
|
|
||||||
if a.doer == nil {
|
|
||||||
return false // no doer, not allowed
|
|
||||||
}
|
|
||||||
e, ok := a.doer.(Enabler)
|
|
||||||
if !ok {
|
|
||||||
return true // not enabler, always allowed
|
|
||||||
}
|
|
||||||
return e.Enabled()
|
|
||||||
}
|
|
||||||
|
|
||||||
// addTrack
|
|
||||||
type addTrack Model
|
|
||||||
|
|
||||||
func (m *Model) AddTrack() Action { return MakeAction((*addTrack)(m)) }
|
|
||||||
func (m *addTrack) Enabled() bool { return m.d.Song.Score.NumVoices() < vm.MAX_VOICES }
|
|
||||||
func (m *addTrack) Do() {
|
|
||||||
defer (*Model)(m).change("AddTrack", SongChange, MajorChange)()
|
|
||||||
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
|
|
||||||
p := sointu.Patch{defaultInstrument.Copy()}
|
|
||||||
t := []sointu.Track{{NumVoices: 1}}
|
|
||||||
_, _, ok := (*Model)(m).addVoices(voiceIndex, p, t, (*Model)(m).linkInstrTrack, true)
|
|
||||||
m.changeCancel = !ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteTrack
|
|
||||||
type deleteTrack Model
|
|
||||||
|
|
||||||
func (m *Model) DeleteTrack() Action { return MakeAction((*deleteTrack)(m)) }
|
|
||||||
func (m *deleteTrack) Enabled() bool { return len(m.d.Song.Score.Tracks) > 0 }
|
|
||||||
func (m *deleteTrack) Do() { (*Model)(m).Tracks().DeleteElements(false) }
|
|
||||||
|
|
||||||
// addInstrument
|
|
||||||
type addInstrument Model
|
|
||||||
|
|
||||||
func (m *Model) AddInstrument() Action { return MakeAction((*addInstrument)(m)) }
|
|
||||||
func (m *addInstrument) Enabled() bool { return (*Model)(m).d.Song.Patch.NumVoices() < vm.MAX_VOICES }
|
|
||||||
func (m *addInstrument) Do() {
|
|
||||||
defer (*Model)(m).change("AddInstrument", SongChange, MajorChange)()
|
|
||||||
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
|
|
||||||
p := sointu.Patch{defaultInstrument.Copy()}
|
|
||||||
t := []sointu.Track{{NumVoices: 1}}
|
|
||||||
_, _, ok := (*Model)(m).addVoices(voiceIndex, p, t, true, (*Model)(m).linkInstrTrack)
|
|
||||||
m.changeCancel = !ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteInstrument
|
|
||||||
type deleteInstrument Model
|
|
||||||
|
|
||||||
func (m *Model) DeleteInstrument() Action { return MakeAction((*deleteInstrument)(m)) }
|
|
||||||
func (m *deleteInstrument) Enabled() bool { return len((*Model)(m).d.Song.Patch) > 0 }
|
|
||||||
func (m *deleteInstrument) Do() { (*Model)(m).Instruments().DeleteElements(false) }
|
|
||||||
|
|
||||||
// splitTrack
|
|
||||||
type splitTrack Model
|
|
||||||
|
|
||||||
func (m *Model) SplitTrack() Action { return MakeAction((*splitTrack)(m)) }
|
|
||||||
func (m *splitTrack) Enabled() bool {
|
|
||||||
return m.d.Cursor.Track >= 0 && m.d.Cursor.Track < len(m.d.Song.Score.Tracks) && m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices > 1
|
|
||||||
}
|
|
||||||
func (m *splitTrack) Do() {
|
|
||||||
defer (*Model)(m).change("SplitTrack", SongChange, MajorChange)()
|
|
||||||
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
|
|
||||||
middle := voiceIndex + (m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices+1)/2
|
|
||||||
end := voiceIndex + m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices
|
|
||||||
left, ok := VoiceSlice(m.d.Song.Score.Tracks, Range{math.MinInt, middle})
|
|
||||||
if !ok {
|
|
||||||
m.changeCancel = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
right, ok := VoiceSlice(m.d.Song.Score.Tracks, Range{end, math.MaxInt})
|
|
||||||
if !ok {
|
|
||||||
m.changeCancel = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
newTrack := sointu.Track{NumVoices: end - middle}
|
|
||||||
m.d.Song.Score.Tracks = append(left, newTrack)
|
|
||||||
m.d.Song.Score.Tracks = append(m.d.Song.Score.Tracks, right...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitInstrument
|
|
||||||
type splitInstrument Model
|
|
||||||
|
|
||||||
func (m *Model) SplitInstrument() Action { return MakeAction((*splitInstrument)(m)) }
|
|
||||||
func (m *splitInstrument) Enabled() bool {
|
|
||||||
return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch) && m.d.Song.Patch[m.d.InstrIndex].NumVoices > 1
|
|
||||||
}
|
|
||||||
func (m *splitInstrument) Do() {
|
|
||||||
defer (*Model)(m).change("SplitInstrument", SongChange, MajorChange)()
|
|
||||||
voiceIndex := m.d.Song.Patch.Copy().FirstVoiceForInstrument(m.d.InstrIndex)
|
|
||||||
middle := voiceIndex + (m.d.Song.Patch[m.d.InstrIndex].NumVoices+1)/2
|
|
||||||
end := voiceIndex + m.d.Song.Patch[m.d.InstrIndex].NumVoices
|
|
||||||
left, ok := VoiceSlice(m.d.Song.Patch, Range{math.MinInt, middle})
|
|
||||||
if !ok {
|
|
||||||
m.changeCancel = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
right, ok := VoiceSlice(m.d.Song.Patch, Range{end, math.MaxInt})
|
|
||||||
if !ok {
|
|
||||||
m.changeCancel = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
newInstrument := defaultInstrument.Copy()
|
|
||||||
(*Model)(m).assignUnitIDs(newInstrument.Units)
|
|
||||||
newInstrument.NumVoices = end - middle
|
|
||||||
m.d.Song.Patch = append(left, newInstrument)
|
|
||||||
m.d.Song.Patch = append(m.d.Song.Patch, right...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addUnit
|
|
||||||
type addUnit struct {
|
|
||||||
Before bool
|
|
||||||
*Model
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) AddUnit(before bool) Action {
|
|
||||||
return MakeAction(addUnit{Before: before, Model: m})
|
|
||||||
}
|
|
||||||
func (a addUnit) Do() {
|
|
||||||
m := (*Model)(a.Model)
|
|
||||||
defer m.change("AddUnitAction", PatchChange, MajorChange)()
|
|
||||||
if len(m.d.Song.Patch) == 0 { // no instruments, add one
|
|
||||||
instr := sointu.Instrument{NumVoices: 1}
|
|
||||||
instr.Units = make([]sointu.Unit, 0, 1)
|
|
||||||
m.d.Song.Patch = append(m.d.Song.Patch, instr)
|
|
||||||
m.d.UnitIndex = 0
|
|
||||||
} else {
|
|
||||||
if !a.Before {
|
|
||||||
m.d.UnitIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.d.InstrIndex = max(min(m.d.InstrIndex, len(m.d.Song.Patch)-1), 0)
|
|
||||||
instr := m.d.Song.Patch[m.d.InstrIndex]
|
|
||||||
newUnits := make([]sointu.Unit, len(instr.Units)+1)
|
|
||||||
m.d.UnitIndex = clamp(m.d.UnitIndex, 0, len(newUnits)-1)
|
|
||||||
m.d.UnitIndex2 = m.d.UnitIndex
|
|
||||||
copy(newUnits, instr.Units[:m.d.UnitIndex])
|
|
||||||
copy(newUnits[m.d.UnitIndex+1:], instr.Units[m.d.UnitIndex:])
|
|
||||||
m.assignUnitIDs(newUnits[m.d.UnitIndex : m.d.UnitIndex+1])
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex].Units = newUnits
|
|
||||||
m.d.ParamIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteUnit
|
|
||||||
type deleteUnit Model
|
|
||||||
|
|
||||||
func (m *Model) DeleteUnit() Action { return MakeAction((*deleteUnit)(m)) }
|
|
||||||
func (m *deleteUnit) Enabled() bool {
|
|
||||||
i := (*Model)(m).d.InstrIndex
|
|
||||||
return i >= 0 && i < len((*Model)(m).d.Song.Patch) && len((*Model)(m).d.Song.Patch[i].Units) > 1
|
|
||||||
}
|
|
||||||
func (m *deleteUnit) Do() {
|
|
||||||
defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)()
|
|
||||||
(*Model)(m).Units().DeleteElements(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// clearUnit
|
|
||||||
type clearUnit Model
|
|
||||||
|
|
||||||
func (m *Model) ClearUnit() Action { return MakeAction((*clearUnit)(m)) }
|
|
||||||
func (m *clearUnit) Enabled() bool {
|
|
||||||
i := (*Model)(m).d.InstrIndex
|
|
||||||
return i >= 0 && i < len(m.d.Song.Patch) && len(m.d.Song.Patch[i].Units) > 0
|
|
||||||
}
|
|
||||||
func (m *clearUnit) Do() {
|
|
||||||
defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)()
|
|
||||||
l := ((*Model)(m)).Units()
|
|
||||||
r := l.listRange()
|
|
||||||
for i := r.Start; i < r.End; i++ {
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex].Units[i] = sointu.Unit{}
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex].Units[i].ID = (*Model)(m).maxID() + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// undo
|
|
||||||
type undo Model
|
|
||||||
|
|
||||||
func (m *Model) Undo() Action { return MakeAction((*undo)(m)) }
|
|
||||||
func (m *undo) Enabled() bool { return len((*Model)(m).undoStack) > 0 }
|
|
||||||
func (m *undo) Do() {
|
|
||||||
m.redoStack = append(m.redoStack, m.d.Copy())
|
|
||||||
if len(m.redoStack) >= maxUndo {
|
|
||||||
copy(m.redoStack, m.redoStack[len(m.redoStack)-maxUndo:])
|
|
||||||
m.redoStack = m.redoStack[:maxUndo]
|
|
||||||
}
|
|
||||||
m.d = m.undoStack[len(m.undoStack)-1]
|
|
||||||
m.undoStack = m.undoStack[:len(m.undoStack)-1]
|
|
||||||
m.prevUndoKind = ""
|
|
||||||
(*Model)(m).updateDeriveData(SongChange)
|
|
||||||
TrySend(m.broker.ToPlayer, any(m.d.Song.Copy()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// redo
|
|
||||||
type redo Model
|
|
||||||
|
|
||||||
func (m *Model) Redo() Action { return MakeAction((*redo)(m)) }
|
|
||||||
func (m *redo) Enabled() bool { return len((*Model)(m).redoStack) > 0 }
|
|
||||||
func (m *redo) Do() {
|
|
||||||
m.undoStack = append(m.undoStack, m.d.Copy())
|
|
||||||
if len(m.undoStack) >= maxUndo {
|
|
||||||
copy(m.undoStack, m.undoStack[len(m.undoStack)-maxUndo:])
|
|
||||||
m.undoStack = m.undoStack[:maxUndo]
|
|
||||||
}
|
|
||||||
m.d = m.redoStack[len(m.redoStack)-1]
|
|
||||||
m.redoStack = m.redoStack[:len(m.redoStack)-1]
|
|
||||||
m.prevUndoKind = ""
|
|
||||||
(*Model)(m).updateDeriveData(SongChange)
|
|
||||||
TrySend(m.broker.ToPlayer, any(m.d.Song.Copy()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddSemiTone
|
|
||||||
type addSemitone Model
|
|
||||||
|
|
||||||
func (m *Model) AddSemitone() Action { return MakeAction((*addSemitone)(m)) }
|
|
||||||
func (m *addSemitone) Do() { Table{(*Notes)(m)}.Add(1, false) }
|
|
||||||
|
|
||||||
// subtractSemitone
|
|
||||||
type subtractSemitone Model
|
|
||||||
|
|
||||||
func (m *Model) SubtractSemitone() Action { return MakeAction((*subtractSemitone)(m)) }
|
|
||||||
func (m *subtractSemitone) Do() { Table{(*Notes)(m)}.Add(-1, false) }
|
|
||||||
|
|
||||||
// addOctave
|
|
||||||
type addOctave Model
|
|
||||||
|
|
||||||
func (m *Model) AddOctave() Action { return MakeAction((*addOctave)(m)) }
|
|
||||||
func (m *addOctave) Do() { Table{(*Notes)(m)}.Add(1, true) }
|
|
||||||
|
|
||||||
// subtractOctave
|
|
||||||
type subtractOctave Model
|
|
||||||
|
|
||||||
func (m *Model) SubtractOctave() Action { return MakeAction((*subtractOctave)(m)) }
|
|
||||||
func (m *subtractOctave) Do() { Table{(*Notes)(m)}.Add(-1, true) }
|
|
||||||
|
|
||||||
// editNoteOff
|
|
||||||
type editNoteOff Model
|
|
||||||
|
|
||||||
func (m *Model) EditNoteOff() Action { return MakeAction((*editNoteOff)(m)) }
|
|
||||||
func (m *editNoteOff) Do() { Table{(*Notes)(m)}.Fill(0) }
|
|
||||||
|
|
||||||
// removeUnused
|
|
||||||
type removeUnused Model
|
|
||||||
|
|
||||||
func (m *Model) RemoveUnused() Action { return MakeAction((*removeUnused)(m)) }
|
|
||||||
func (m *removeUnused) Do() {
|
|
||||||
defer (*Model)(m).change("RemoveUnusedAction", ScoreChange, MajorChange)()
|
|
||||||
for trkIndex, trk := range m.d.Song.Score.Tracks {
|
|
||||||
// assign new indices to patterns
|
|
||||||
newIndex := map[int]int{}
|
|
||||||
runningIndex := 0
|
|
||||||
length := 0
|
|
||||||
if len(trk.Order) > m.d.Song.Score.Length {
|
|
||||||
trk.Order = trk.Order[:m.d.Song.Score.Length]
|
|
||||||
}
|
|
||||||
for i, p := range trk.Order {
|
|
||||||
// if the pattern hasn't been considered and is within limits
|
|
||||||
if _, ok := newIndex[p]; !ok && p >= 0 && p < len(trk.Patterns) {
|
|
||||||
pat := trk.Patterns[p]
|
|
||||||
useful := false
|
|
||||||
for _, n := range pat { // patterns that have anything else than all holds are useful and to be kept
|
|
||||||
if n != 1 {
|
|
||||||
useful = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if useful {
|
|
||||||
newIndex[p] = runningIndex
|
|
||||||
runningIndex++
|
|
||||||
} else {
|
|
||||||
newIndex[p] = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ind, ok := newIndex[p]; ok && ind > -1 {
|
|
||||||
length = i + 1
|
|
||||||
trk.Order[i] = ind
|
|
||||||
} else {
|
|
||||||
trk.Order[i] = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trk.Order = trk.Order[:length]
|
|
||||||
newPatterns := make([]sointu.Pattern, runningIndex)
|
|
||||||
for i, pat := range trk.Patterns {
|
|
||||||
if ind, ok := newIndex[i]; ok && ind > -1 {
|
|
||||||
patLength := 0
|
|
||||||
for j, note := range pat { // find last note that is something else that hold
|
|
||||||
if note != 1 {
|
|
||||||
patLength = j + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if patLength > m.d.Song.Score.RowsPerPattern {
|
|
||||||
patLength = m.d.Song.Score.RowsPerPattern
|
|
||||||
}
|
|
||||||
newPatterns[ind] = pat[:patLength] // crop to either RowsPerPattern or last row having something else than hold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trk.Patterns = newPatterns
|
|
||||||
m.d.Song.Score.Tracks[trkIndex] = trk
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// playCurrentPos
|
|
||||||
type playCurrentPos Model
|
|
||||||
|
|
||||||
func (m *Model) PlayCurrentPos() Action { return MakeAction((*playCurrentPos)(m)) }
|
|
||||||
func (m *playCurrentPos) Enabled() bool { return !m.instrEnlarged }
|
|
||||||
func (m *playCurrentPos) Do() {
|
|
||||||
(*Model)(m).setPanic(false)
|
|
||||||
(*Model)(m).setLoop(Loop{})
|
|
||||||
m.playing = true
|
|
||||||
TrySend(m.broker.ToPlayer, any(StartPlayMsg{m.d.Cursor.SongPos}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// playSongStart
|
|
||||||
type playSongStart Model
|
|
||||||
|
|
||||||
func (m *Model) PlaySongStart() Action { return MakeAction((*playSongStart)(m)) }
|
|
||||||
func (m *playSongStart) Enabled() bool { return !m.instrEnlarged }
|
|
||||||
func (m *playSongStart) Do() {
|
|
||||||
(*Model)(m).setPanic(false)
|
|
||||||
(*Model)(m).setLoop(Loop{})
|
|
||||||
m.playing = true
|
|
||||||
TrySend(m.broker.ToPlayer, any(StartPlayMsg{}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// playSelected
|
|
||||||
type playSelected Model
|
|
||||||
|
|
||||||
func (m *Model) PlaySelected() Action { return MakeAction((*playSelected)(m)) }
|
|
||||||
func (m *playSelected) Enabled() bool { return !m.instrEnlarged }
|
|
||||||
func (m *playSelected) Do() {
|
|
||||||
(*Model)(m).setPanic(false)
|
|
||||||
m.playing = true
|
|
||||||
l := (*Model)(m).OrderRows()
|
|
||||||
r := l.listRange()
|
|
||||||
newLoop := Loop{r.Start, r.End - r.Start}
|
|
||||||
(*Model)(m).setLoop(newLoop)
|
|
||||||
TrySend(m.broker.ToPlayer, any(StartPlayMsg{sointu.SongPos{OrderRow: r.Start, PatternRow: 0}}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// playFromLoopStart
|
|
||||||
type playFromLoopStart Model
|
|
||||||
|
|
||||||
func (m *Model) PlayFromLoopStart() Action { return MakeAction((*playFromLoopStart)(m)) }
|
|
||||||
func (m *playFromLoopStart) Enabled() bool { return !m.instrEnlarged }
|
|
||||||
func (m *playFromLoopStart) Do() {
|
|
||||||
(*Model)(m).setPanic(false)
|
|
||||||
if m.loop == (Loop{}) {
|
|
||||||
(*Model)(m).PlaySelected().Do()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.playing = true
|
|
||||||
TrySend(m.broker.ToPlayer, any(StartPlayMsg{sointu.SongPos{OrderRow: m.loop.Start, PatternRow: 0}}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// stopPlaying
|
|
||||||
type stopPlaying Model
|
|
||||||
|
|
||||||
func (m *Model) StopPlaying() Action { return MakeAction((*stopPlaying)(m)) }
|
|
||||||
func (m *stopPlaying) Do() {
|
|
||||||
if !m.playing {
|
|
||||||
(*Model)(m).setPanic(true)
|
|
||||||
(*Model)(m).setLoop(Loop{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.playing = false
|
|
||||||
TrySend(m.broker.ToPlayer, any(IsPlayingMsg{false}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// addOrderRow
|
|
||||||
type addOrderRow struct {
|
|
||||||
Before bool
|
|
||||||
*Model
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) AddOrderRow(before bool) Action {
|
|
||||||
return MakeAction(addOrderRow{Before: before, Model: m})
|
|
||||||
}
|
|
||||||
func (a addOrderRow) Do() {
|
|
||||||
m := a.Model
|
|
||||||
defer m.change("AddOrderRowAction", ScoreChange, MinorChange)()
|
|
||||||
if !a.Before {
|
|
||||||
m.d.Cursor.OrderRow++
|
|
||||||
}
|
|
||||||
m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow
|
|
||||||
from := m.d.Cursor.OrderRow
|
|
||||||
m.d.Song.Score.Length++
|
|
||||||
for i := range m.d.Song.Score.Tracks {
|
|
||||||
order := &m.d.Song.Score.Tracks[i].Order
|
|
||||||
if len(*order) > from {
|
|
||||||
*order = append(*order, -1)
|
|
||||||
copy((*order)[from+1:], (*order)[from:])
|
|
||||||
(*order)[from] = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteOrderRow
|
|
||||||
type deleteOrderRow struct {
|
|
||||||
Backwards bool
|
|
||||||
*Model
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) DeleteOrderRow(backwards bool) Action {
|
|
||||||
return MakeAction(deleteOrderRow{Backwards: backwards, Model: m})
|
|
||||||
}
|
|
||||||
func (d deleteOrderRow) Do() {
|
|
||||||
m := d.Model
|
|
||||||
defer m.change("AddOrderRowAction", ScoreChange, MinorChange)()
|
|
||||||
from := m.d.Cursor.OrderRow
|
|
||||||
m.d.Song.Score.Length--
|
|
||||||
for i := range m.d.Song.Score.Tracks {
|
|
||||||
order := &m.d.Song.Score.Tracks[i].Order
|
|
||||||
if len(*order) > from {
|
|
||||||
copy((*order)[from:], (*order)[from+1:])
|
|
||||||
*order = (*order)[:len(*order)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if d.Backwards {
|
|
||||||
if m.d.Cursor.OrderRow > 0 {
|
|
||||||
m.d.Cursor.OrderRow--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow
|
|
||||||
}
|
|
||||||
|
|
||||||
// chooseSendSource
|
|
||||||
type chooseSendSource struct {
|
|
||||||
ID int
|
|
||||||
*Model
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) IsChoosingSendTarget() bool {
|
|
||||||
return m.d.SendSource > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ChooseSendSource(id int) Action {
|
|
||||||
return MakeAction(chooseSendSource{ID: id, Model: m})
|
|
||||||
}
|
|
||||||
func (s chooseSendSource) Do() {
|
|
||||||
defer (*Model)(s.Model).change("ChooseSendSource", NoChange, MinorChange)()
|
|
||||||
if s.Model.d.SendSource == s.ID {
|
|
||||||
s.Model.d.SendSource = 0 // unselect
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.Model.d.SendSource = s.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// chooseSendTarget
|
|
||||||
type chooseSendTarget struct {
|
|
||||||
ID int
|
|
||||||
Port int
|
|
||||||
*Model
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ChooseSendTarget(id int, port int) Action {
|
|
||||||
return MakeAction(chooseSendTarget{ID: id, Port: port, Model: m})
|
|
||||||
}
|
|
||||||
func (s chooseSendTarget) Do() {
|
|
||||||
defer (*Model)(s.Model).change("ChooseSendTarget", SongChange, MinorChange)()
|
|
||||||
sourceID := (*Model)(s.Model).d.SendSource
|
|
||||||
s.d.SendSource = 0
|
|
||||||
if sourceID <= 0 || s.ID <= 0 || s.Port < 0 || s.Port > 7 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
si, su, err := s.d.Song.Patch.FindUnit(sourceID)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.d.Song.Patch[si].Units[su].Parameters["target"] = s.ID
|
|
||||||
s.d.Song.Patch[si].Units[su].Parameters["port"] = s.Port
|
|
||||||
}
|
|
||||||
|
|
||||||
// newSong
|
|
||||||
type newSong Model
|
|
||||||
|
|
||||||
func (m *Model) NewSong() Action { return MakeAction((*newSong)(m)) }
|
|
||||||
func (m *newSong) Do() {
|
|
||||||
m.dialog = NewSongChanges
|
|
||||||
(*Model)(m).completeAction(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// openSong
|
|
||||||
type openSong Model
|
|
||||||
|
|
||||||
func (m *Model) OpenSong() Action { return MakeAction((*openSong)(m)) }
|
|
||||||
func (m *openSong) Do() {
|
|
||||||
m.dialog = OpenSongChanges
|
|
||||||
(*Model)(m).completeAction(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// requestQuit
|
|
||||||
type requestQuit Model
|
|
||||||
|
|
||||||
func (m *Model) RequestQuit() Action { return MakeAction((*requestQuit)(m)) }
|
|
||||||
func (m *requestQuit) Do() {
|
|
||||||
if !m.quitted {
|
|
||||||
m.dialog = QuitChanges
|
|
||||||
(*Model)(m).completeAction(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// forceQuit
|
|
||||||
type forceQuit Model
|
|
||||||
|
|
||||||
func (m *Model) ForceQuit() Action { return MakeAction((*forceQuit)(m)) }
|
|
||||||
func (m *forceQuit) Do() { m.quitted = true }
|
|
||||||
|
|
||||||
// saveSong
|
|
||||||
type saveSong Model
|
|
||||||
|
|
||||||
func (m *Model) SaveSong() Action { return MakeAction((*saveSong)(m)) }
|
|
||||||
func (m *saveSong) Do() {
|
|
||||||
if m.d.FilePath == "" {
|
|
||||||
switch m.dialog {
|
|
||||||
case NoDialog:
|
|
||||||
m.dialog = SaveAsExplorer
|
|
||||||
case NewSongChanges:
|
|
||||||
m.dialog = NewSongSaveExplorer
|
|
||||||
case OpenSongChanges:
|
|
||||||
m.dialog = OpenSongSaveExplorer
|
|
||||||
case QuitChanges:
|
|
||||||
m.dialog = QuitSaveExplorer
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f, err := os.Create(m.d.FilePath)
|
|
||||||
if err != nil {
|
|
||||||
(*Model)(m).Alerts().Add("Error creating file: "+err.Error(), Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
(*Model)(m).WriteSong(f)
|
|
||||||
m.d.ChangedSinceSave = false
|
|
||||||
}
|
|
||||||
|
|
||||||
type discardSong Model
|
|
||||||
|
|
||||||
func (m *Model) DiscardSong() Action { return MakeAction((*discardSong)(m)) }
|
|
||||||
func (m *discardSong) Do() { (*Model)(m).completeAction(false) }
|
|
||||||
|
|
||||||
type saveSongAs Model
|
|
||||||
|
|
||||||
func (m *Model) SaveSongAs() Action { return MakeAction((*saveSongAs)(m)) }
|
|
||||||
func (m *saveSongAs) Do() { m.dialog = SaveAsExplorer }
|
|
||||||
|
|
||||||
type cancel Model
|
|
||||||
|
|
||||||
func (m *Model) Cancel() Action { return MakeAction((*cancel)(m)) }
|
|
||||||
func (m *cancel) Do() { m.dialog = NoDialog }
|
|
||||||
|
|
||||||
type exportAction Model
|
|
||||||
|
|
||||||
func (m *Model) Export() Action { return MakeAction((*exportAction)(m)) }
|
|
||||||
func (m *exportAction) Do() { m.dialog = Export }
|
|
||||||
|
|
||||||
type exportFloat Model
|
|
||||||
|
|
||||||
func (m *Model) ExportFloat() Action { return MakeAction((*exportFloat)(m)) }
|
|
||||||
func (m *exportFloat) Do() { m.dialog = ExportFloatExplorer }
|
|
||||||
|
|
||||||
type ExportInt16 Model
|
|
||||||
|
|
||||||
func (m *Model) ExportInt16() Action { return MakeAction((*ExportInt16)(m)) }
|
|
||||||
func (m *ExportInt16) Do() { m.dialog = ExportInt16Explorer }
|
|
||||||
|
|
||||||
type showLicense Model
|
|
||||||
|
|
||||||
func (m *Model) ShowLicense() Action { return MakeAction((*showLicense)(m)) }
|
|
||||||
func (m *showLicense) Do() { m.dialog = License }
|
|
||||||
|
|
||||||
type selectMidiInput struct {
|
|
||||||
Item string
|
|
||||||
*Model
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SelectMidiInput(item string) Action {
|
|
||||||
return MakeAction(selectMidiInput{Item: item, Model: m})
|
|
||||||
}
|
|
||||||
func (s selectMidiInput) Do() {
|
|
||||||
m := s.Model
|
|
||||||
if err := s.Model.MIDI.Open(s.Item); err == nil {
|
|
||||||
message := fmt.Sprintf("Opened MIDI device: %s", s.Item)
|
|
||||||
m.Alerts().Add(message, Info)
|
|
||||||
} else {
|
|
||||||
message := fmt.Sprintf("Could not open MIDI device: %s", s.Item)
|
|
||||||
m.Alerts().Add(message, Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) completeAction(checkSave bool) {
|
|
||||||
if checkSave && m.d.ChangedSinceSave {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch m.dialog {
|
|
||||||
case NewSongChanges, NewSongSaveExplorer:
|
|
||||||
c := m.change("NewSong", SongChange, MajorChange)
|
|
||||||
m.resetSong()
|
|
||||||
m.setLoop(Loop{})
|
|
||||||
c()
|
|
||||||
m.d.ChangedSinceSave = false
|
|
||||||
m.dialog = NoDialog
|
|
||||||
case OpenSongChanges, OpenSongSaveExplorer:
|
|
||||||
m.dialog = OpenSongOpenExplorer
|
|
||||||
case QuitChanges, QuitSaveExplorer:
|
|
||||||
m.quitted = true
|
|
||||||
m.dialog = NoDialog
|
|
||||||
default:
|
|
||||||
m.dialog = NoDialog
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) setPanic(val bool) {
|
|
||||||
if m.panic != val {
|
|
||||||
m.panic = val
|
|
||||||
TrySend(m.broker.ToPlayer, any(PanicMsg{val}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) setLoop(newLoop Loop) {
|
|
||||||
if m.loop != newLoop {
|
|
||||||
m.loop = newLoop
|
|
||||||
TrySend(m.broker.ToPlayer, any(newLoop))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,9 +17,7 @@ type (
|
|||||||
FadeLevel float64
|
FadeLevel float64
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertPriority int
|
AlertPriority int
|
||||||
AlertYieldFunc func(index int, alert Alert) bool
|
|
||||||
Alerts Model
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -29,12 +27,12 @@ const (
|
|||||||
Error
|
Error
|
||||||
)
|
)
|
||||||
|
|
||||||
// Model methods
|
// Alerts returns the Alerts model from the main Model, used to manage alerts.
|
||||||
|
|
||||||
func (m *Model) Alerts() *Alerts { return (*Alerts)(m) }
|
func (m *Model) Alerts() *Alerts { return (*Alerts)(m) }
|
||||||
|
|
||||||
// Alerts methods
|
type Alerts Model
|
||||||
|
|
||||||
|
// Iterate through the alerts.
|
||||||
func (m *Alerts) Iterate(yield func(index int, alert Alert) bool) {
|
func (m *Alerts) Iterate(yield func(index int, alert Alert) bool) {
|
||||||
for i, a := range m.alerts {
|
for i, a := range m.alerts {
|
||||||
if !yield(i, a) {
|
if !yield(i, a) {
|
||||||
@@ -43,6 +41,8 @@ func (m *Alerts) Iterate(yield func(index int, alert Alert) bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the alerts, reducing their duration and updating their fade levels,
|
||||||
|
// given the elapsed time d.
|
||||||
func (m *Alerts) Update(d time.Duration) (animating bool) {
|
func (m *Alerts) Update(d time.Duration) (animating bool) {
|
||||||
for i := len(m.alerts) - 1; i >= 0; i-- {
|
for i := len(m.alerts) - 1; i >= 0; i-- {
|
||||||
if m.alerts[i].Duration >= d {
|
if m.alerts[i].Duration >= d {
|
||||||
@@ -66,6 +66,7 @@ func (m *Alerts) Update(d time.Duration) (animating bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a new alert with the given message and priority.
|
||||||
func (m *Alerts) Add(message string, priority AlertPriority) {
|
func (m *Alerts) Add(message string, priority AlertPriority) {
|
||||||
m.AddAlert(Alert{
|
m.AddAlert(Alert{
|
||||||
Priority: priority,
|
Priority: priority,
|
||||||
@@ -74,6 +75,7 @@ func (m *Alerts) Add(message string, priority AlertPriority) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddNamed adds a new alert with the given name, message, and priority.
|
||||||
func (m *Alerts) AddNamed(name, message string, priority AlertPriority) {
|
func (m *Alerts) AddNamed(name, message string, priority AlertPriority) {
|
||||||
m.AddAlert(Alert{
|
m.AddAlert(Alert{
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -83,6 +85,7 @@ func (m *Alerts) AddNamed(name, message string, priority AlertPriority) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearNamed clears the alert with the given name.
|
||||||
func (m *Alerts) ClearNamed(name string) {
|
func (m *Alerts) ClearNamed(name string) {
|
||||||
for i := range m.alerts {
|
for i := range m.alerts {
|
||||||
if n := m.alerts[i].Name; n != "" && n == name {
|
if n := m.alerts[i].Name; n != "" && n == name {
|
||||||
@@ -92,6 +95,7 @@ func (m *Alerts) ClearNamed(name string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddAlert adds or updates an alert.
|
||||||
func (m *Alerts) AddAlert(a Alert) {
|
func (m *Alerts) AddAlert(a Alert) {
|
||||||
for i := range m.alerts {
|
for i := range m.alerts {
|
||||||
if n := m.alerts[i].Name; n != "" && n == a.Name {
|
if n := m.alerts[i].Name; n != "" && n == a.Name {
|
||||||
|
|||||||
550
tracker/basic_types.go
Normal file
550
tracker/basic_types.go
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"iter"
|
||||||
|
"math"
|
||||||
|
"math/bits"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enabler is an interface that defines a single Enabled() method, which is used
|
||||||
|
// by the UI to check if UI Action/Bool/Int etc. is enabled or not.
|
||||||
|
type Enabler interface {
|
||||||
|
Enabled() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Action describes a user action that can be performed on the model, which
|
||||||
|
// can be initiated by calling the Do() method. It is usually initiated by a
|
||||||
|
// button press or a menu item. Action advertises whether it is enabled, so
|
||||||
|
// UI can e.g. gray out buttons when the underlying action is not allowed.
|
||||||
|
// The underlying Doer can optionally implement the Enabler interface to
|
||||||
|
// decide if the action is enabled or not; if it does not implement the
|
||||||
|
// Enabler interface, the action is always allowed.
|
||||||
|
Action struct {
|
||||||
|
doer Doer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doer is an interface that defines a single Do() method, which is called
|
||||||
|
// when an action is performed.
|
||||||
|
Doer interface {
|
||||||
|
Do()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeAction(doer Doer) Action { return Action{doer: doer} }
|
||||||
|
|
||||||
|
func (a Action) Do() {
|
||||||
|
e, ok := a.doer.(Enabler)
|
||||||
|
if ok && !e.Enabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if a.doer != nil {
|
||||||
|
a.doer.Do()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Action) Enabled() bool {
|
||||||
|
if a.doer == nil {
|
||||||
|
return false // no doer, not allowed
|
||||||
|
}
|
||||||
|
e, ok := a.doer.(Enabler)
|
||||||
|
if !ok {
|
||||||
|
return true // not enabler, always allowed
|
||||||
|
}
|
||||||
|
return e.Enabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool
|
||||||
|
|
||||||
|
type (
|
||||||
|
Bool struct {
|
||||||
|
value BoolValue
|
||||||
|
}
|
||||||
|
|
||||||
|
BoolValue interface {
|
||||||
|
Value() bool
|
||||||
|
SetValue(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
simpleBool bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeBool(value BoolValue) Bool { return Bool{value: value} }
|
||||||
|
func MakeBoolFromPtr(value *bool) Bool { return Bool{value: (*simpleBool)(value)} }
|
||||||
|
func (v Bool) Toggle() { v.SetValue(!v.Value()) }
|
||||||
|
|
||||||
|
func (v Bool) SetValue(value bool) (changed bool) {
|
||||||
|
if !v.Enabled() || v.Value() == value {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v.value.SetValue(value)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Bool) Value() bool {
|
||||||
|
if v.value == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return v.value.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Bool) Enabled() bool {
|
||||||
|
if v.value == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
e, ok := v.value.(Enabler)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return e.Enabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *simpleBool) Value() bool { return bool(*v) }
|
||||||
|
func (v *simpleBool) SetValue(value bool) { *v = simpleBool(value) }
|
||||||
|
|
||||||
|
// Int
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Int represents an integer value in the tracker model e.g. BPM, song
|
||||||
|
// length, etc. It is a wrapper around an IntValue interface that provides
|
||||||
|
// methods to manipulate the value, but Int guard that all changes are
|
||||||
|
// within the range of the underlying IntValue implementation and that
|
||||||
|
// SetValue is not called when the value is unchanged.
|
||||||
|
Int struct {
|
||||||
|
value IntValue
|
||||||
|
}
|
||||||
|
|
||||||
|
IntValue interface {
|
||||||
|
Value() int
|
||||||
|
SetValue(int) (changed bool)
|
||||||
|
Range() RangeInclusive
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeInt(value IntValue) Int { return Int{value} }
|
||||||
|
|
||||||
|
func (v Int) Add(delta int) (changed bool) {
|
||||||
|
return v.SetValue(v.Value() + delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Int) SetValue(value int) (changed bool) {
|
||||||
|
r := v.Range()
|
||||||
|
value = r.Clamp(value)
|
||||||
|
if value == v.Value() || value < r.Min || value > r.Max {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return v.value.SetValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Int) Range() RangeInclusive {
|
||||||
|
if v.value == nil {
|
||||||
|
return RangeInclusive{0, 0}
|
||||||
|
}
|
||||||
|
return v.value.Range()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Int) Value() int {
|
||||||
|
if v.value == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return v.value.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String
|
||||||
|
|
||||||
|
type (
|
||||||
|
String struct {
|
||||||
|
value StringValue
|
||||||
|
}
|
||||||
|
|
||||||
|
StringValue interface {
|
||||||
|
Value() string
|
||||||
|
SetValue(string) (changed bool)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeString(value StringValue) String { return String{value: value} }
|
||||||
|
|
||||||
|
func (v String) SetValue(value string) (changed bool) {
|
||||||
|
if v.value == nil || v.value.Value() == value {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return v.value.SetValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v String) Value() string {
|
||||||
|
if v.value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return v.value.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// List
|
||||||
|
|
||||||
|
type (
|
||||||
|
List struct {
|
||||||
|
data ListData
|
||||||
|
}
|
||||||
|
|
||||||
|
ListData interface {
|
||||||
|
Selected() int
|
||||||
|
Selected2() int
|
||||||
|
SetSelected(int)
|
||||||
|
SetSelected2(int)
|
||||||
|
Count() int
|
||||||
|
}
|
||||||
|
|
||||||
|
MutableListData interface {
|
||||||
|
Change(kind string, severity ChangeSeverity) func()
|
||||||
|
Cancel()
|
||||||
|
Move(r Range, delta int) (ok bool)
|
||||||
|
Delete(r Range) (ok bool)
|
||||||
|
Marshal(r Range) ([]byte, error)
|
||||||
|
Unmarshal([]byte) (r Range, err error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeList(data ListData) List { return List{data} }
|
||||||
|
|
||||||
|
func (l List) Selected() int { return max(min(l.data.Selected(), l.data.Count()-1), 0) }
|
||||||
|
func (l List) Selected2() int { return max(min(l.data.Selected2(), l.data.Count()-1), 0) }
|
||||||
|
func (l List) SetSelected(value int) { l.data.SetSelected(max(min(value, l.data.Count()-1), 0)) }
|
||||||
|
func (l List) SetSelected2(value int) { l.data.SetSelected2(max(min(value, l.data.Count()-1), 0)) }
|
||||||
|
func (l List) Count() int { return l.data.Count() }
|
||||||
|
|
||||||
|
// MoveElements moves the selected elements in a list by delta. The list must
|
||||||
|
// implement the MutableListData interface.
|
||||||
|
func (v List) MoveElements(delta int) bool {
|
||||||
|
s, ok := v.data.(MutableListData)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
r := v.listRange()
|
||||||
|
if delta == 0 || r.Start+delta < 0 || r.End+delta > v.Count() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer s.Change("MoveElements", MajorChange)()
|
||||||
|
if !s.Move(r, delta) {
|
||||||
|
s.Cancel()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v.SetSelected(v.Selected() + delta)
|
||||||
|
v.SetSelected2(v.Selected2() + delta)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteElements deletes the selected elements in a list. The list must
|
||||||
|
// implement the MutableListData interface.
|
||||||
|
func (v List) DeleteElements(backwards bool) bool {
|
||||||
|
d, ok := v.data.(MutableListData)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
r := v.listRange()
|
||||||
|
if r.Len() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer d.Change("DeleteElements", MajorChange)()
|
||||||
|
if !d.Delete(r) {
|
||||||
|
d.Cancel()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if backwards && r.Start > 0 {
|
||||||
|
r.Start--
|
||||||
|
}
|
||||||
|
v.SetSelected(r.Start)
|
||||||
|
v.SetSelected2(r.Start)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyElements copies the selected elements in a list. The list must implement
|
||||||
|
// the MutableListData interface. Returns the copied data, marshaled into byte
|
||||||
|
// slice, and true if successful.
|
||||||
|
func (v List) CopyElements() ([]byte, bool) {
|
||||||
|
m, ok := v.data.(MutableListData)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
r := v.listRange()
|
||||||
|
if r.Len() == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
ret, err := m.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return ret, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PasteElements pastes the data into the list. The data is unmarshaled from the
|
||||||
|
// byte slice. The list must implement the MutableListData interface. Returns
|
||||||
|
// true if successful.
|
||||||
|
func (v List) PasteElements(data []byte) (ok bool) {
|
||||||
|
m, ok := v.data.(MutableListData)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer m.Change("PasteElements", MajorChange)()
|
||||||
|
r, err := m.Unmarshal(data)
|
||||||
|
if err != nil {
|
||||||
|
m.Cancel()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v.SetSelected(r.Start)
|
||||||
|
v.SetSelected2(r.End - 1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v List) Mutable() bool {
|
||||||
|
_, ok := v.data.(MutableListData)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *List) listRange() (r Range) {
|
||||||
|
r.Start = max(min(v.Selected(), v.Selected2()), 0)
|
||||||
|
r.End = min(max(v.Selected(), v.Selected2())+1, v.Count())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeInclusive
|
||||||
|
|
||||||
|
// RangeInclusive represents a range of integers [Min, Max], inclusive.
|
||||||
|
type RangeInclusive struct{ Min, Max int }
|
||||||
|
|
||||||
|
func (r RangeInclusive) Clamp(value int) int { return max(min(value, r.Max), r.Min) }
|
||||||
|
|
||||||
|
// Range is used to represent a range [Start,End) of integers, excluding End
|
||||||
|
type Range struct{ Start, End int }
|
||||||
|
|
||||||
|
func (r Range) Len() int { return r.End - r.Start }
|
||||||
|
|
||||||
|
func (r Range) Swaps(delta int) iter.Seq2[int, int] {
|
||||||
|
if delta > 0 {
|
||||||
|
return func(yield func(int, int) bool) {
|
||||||
|
for i := r.End - 1; i >= r.Start; i-- {
|
||||||
|
if !yield(i, i+delta) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return func(yield func(int, int) bool) {
|
||||||
|
for i := r.Start; i < r.End; i++ {
|
||||||
|
if !yield(i, i+delta) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Range) Intersect(s Range) (ret Range) {
|
||||||
|
ret.Start = max(r.Start, s.Start)
|
||||||
|
ret.End = max(min(r.End, s.End), ret.Start)
|
||||||
|
if ret.Len() == 0 {
|
||||||
|
return Range{}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeMoveRanges(a Range, delta int) [4]Range {
|
||||||
|
if delta < 0 {
|
||||||
|
return [4]Range{
|
||||||
|
{math.MinInt, a.Start + delta},
|
||||||
|
{a.Start, a.End},
|
||||||
|
{a.Start + delta, a.Start},
|
||||||
|
{a.End, math.MaxInt},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [4]Range{
|
||||||
|
{math.MinInt, a.Start},
|
||||||
|
{a.End, a.End + delta},
|
||||||
|
{a.Start, a.End},
|
||||||
|
{a.End + delta, math.MaxInt},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeSetLength takes a range and a length, and returns a slice of ranges that
|
||||||
|
// can be used with VoiceSlice to expand or shrink the range to the given
|
||||||
|
// length, by either duplicating or removing elements. The function tries to
|
||||||
|
// duplicate elements so all elements are equally spaced, and tries to remove
|
||||||
|
// elements from the middle of the range.
|
||||||
|
func MakeSetLength(a Range, length int) []Range {
|
||||||
|
if length <= 0 || a.Len() <= 0 {
|
||||||
|
return []Range{{a.Start, a.Start}}
|
||||||
|
}
|
||||||
|
ret := make([]Range, a.Len(), max(a.Len(), length)+2)
|
||||||
|
for i := 0; i < a.Len(); i++ {
|
||||||
|
ret[i] = Range{a.Start + i, a.Start + i + 1}
|
||||||
|
}
|
||||||
|
for x := len(ret); x < length; x++ {
|
||||||
|
e := (x << 1) ^ (1 << bits.Len((uint)(x)))
|
||||||
|
ret = append(ret[0:e+1], ret[e:]...)
|
||||||
|
}
|
||||||
|
for x := len(ret); x > length; x-- {
|
||||||
|
e := (((x << 1) ^ (1 << bits.Len((uint)(x)))) + x - 1) % x
|
||||||
|
ret = append(ret[0:e], ret[e+1:]...)
|
||||||
|
}
|
||||||
|
ret = append([]Range{{math.MinInt, a.Start}}, ret...)
|
||||||
|
ret = append(ret, Range{a.End, math.MaxInt})
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func Complement(a Range) [2]Range {
|
||||||
|
return [2]Range{
|
||||||
|
{math.MinInt, a.Start},
|
||||||
|
{a.End, math.MaxInt},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert inserts elements into a slice at the given index. If the index is out
|
||||||
|
// of bounds, the function returns false.
|
||||||
|
func Insert[T any, S ~[]T](slice S, index int, inserted ...T) (ret S, ok bool) {
|
||||||
|
if index < 0 || index > len(slice) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
ret = make(S, 0, len(slice)+len(inserted))
|
||||||
|
ret = append(ret, slice[:index]...)
|
||||||
|
ret = append(ret, inserted...)
|
||||||
|
ret = append(ret, slice[index:]...)
|
||||||
|
return ret, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table
|
||||||
|
|
||||||
|
type (
|
||||||
|
Table struct {
|
||||||
|
TableData
|
||||||
|
}
|
||||||
|
|
||||||
|
TableData interface {
|
||||||
|
Cursor() Point
|
||||||
|
Cursor2() Point
|
||||||
|
SetCursor(Point)
|
||||||
|
SetCursor2(Point)
|
||||||
|
Width() int
|
||||||
|
Height() int
|
||||||
|
MoveCursor(dx, dy int) (ok bool)
|
||||||
|
|
||||||
|
clear(p Point)
|
||||||
|
set(p Point, value int)
|
||||||
|
add(rect Rect, delta int, largestep bool) (ok bool)
|
||||||
|
marshal(rect Rect) (data []byte, ok bool)
|
||||||
|
unmarshalAtCursor(data []byte) (ok bool)
|
||||||
|
unmarshalRange(rect Rect, data []byte) (ok bool)
|
||||||
|
change(kind string, severity ChangeSeverity) func()
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
Point struct {
|
||||||
|
X, Y int
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect struct {
|
||||||
|
TopLeft, BottomRight Point
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rect methods
|
||||||
|
|
||||||
|
func (r *Rect) Contains(p Point) bool {
|
||||||
|
return r.TopLeft.X <= p.X && p.X <= r.BottomRight.X &&
|
||||||
|
r.TopLeft.Y <= p.Y && p.Y <= r.BottomRight.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rect) Width() int {
|
||||||
|
return r.BottomRight.X - r.TopLeft.X + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rect) Height() int {
|
||||||
|
return r.BottomRight.Y - r.TopLeft.Y + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rect) Limit(width, height int) {
|
||||||
|
if r.TopLeft.X < 0 {
|
||||||
|
r.TopLeft.X = 0
|
||||||
|
}
|
||||||
|
if r.TopLeft.Y < 0 {
|
||||||
|
r.TopLeft.Y = 0
|
||||||
|
}
|
||||||
|
if r.BottomRight.X >= width {
|
||||||
|
r.BottomRight.X = width - 1
|
||||||
|
}
|
||||||
|
if r.BottomRight.Y >= height {
|
||||||
|
r.BottomRight.Y = height - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Table) Range() (rect Rect) {
|
||||||
|
rect.TopLeft.X = min(v.Cursor().X, v.Cursor2().X)
|
||||||
|
rect.TopLeft.Y = min(v.Cursor().Y, v.Cursor2().Y)
|
||||||
|
rect.BottomRight.X = max(v.Cursor().X, v.Cursor2().X)
|
||||||
|
rect.BottomRight.Y = max(v.Cursor().Y, v.Cursor2().Y)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Table) Copy() ([]byte, bool) {
|
||||||
|
ret, ok := v.marshal(v.Range())
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return ret, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Table) Paste(data []byte) bool {
|
||||||
|
defer v.change("Paste", MajorChange)()
|
||||||
|
if v.Cursor() == v.Cursor2() {
|
||||||
|
return v.unmarshalAtCursor(data)
|
||||||
|
} else {
|
||||||
|
return v.unmarshalRange(v.Range(), data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Table) Clear() {
|
||||||
|
defer v.change("Clear", MajorChange)()
|
||||||
|
rect := v.Range()
|
||||||
|
rect.Limit(v.Width(), v.Height())
|
||||||
|
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
||||||
|
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
||||||
|
v.clear(Point{x, y})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Table) Set(value byte) {
|
||||||
|
defer v.change("Set", MajorChange)()
|
||||||
|
cursor := v.Cursor()
|
||||||
|
// TODO: might check for visibility
|
||||||
|
v.set(cursor, int(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Table) Fill(value int) {
|
||||||
|
defer v.change("Fill", MajorChange)()
|
||||||
|
rect := v.Range()
|
||||||
|
rect.Limit(v.Width(), v.Height())
|
||||||
|
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
||||||
|
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
||||||
|
v.set(Point{x, y}, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Table) Add(delta int, largeStep bool) {
|
||||||
|
defer v.change("Add", MinorChange)()
|
||||||
|
if !v.add(v.Range(), delta, largeStep) {
|
||||||
|
v.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Table) SetCursorX(x int) {
|
||||||
|
p := v.Cursor()
|
||||||
|
p.X = x
|
||||||
|
v.SetCursor(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Table) SetCursorY(y int) {
|
||||||
|
p := v.Cursor()
|
||||||
|
p.Y = y
|
||||||
|
v.SetCursor(p)
|
||||||
|
}
|
||||||
382
tracker/bool.go
382
tracker/bool.go
@@ -1,382 +0,0 @@
|
|||||||
package tracker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Bool struct {
|
|
||||||
value BoolValue
|
|
||||||
}
|
|
||||||
|
|
||||||
BoolValue interface {
|
|
||||||
Value() bool
|
|
||||||
SetValue(bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
Panic Model
|
|
||||||
IsRecording Model
|
|
||||||
Playing Model
|
|
||||||
Effect Model
|
|
||||||
TrackMidiIn Model
|
|
||||||
UnitSearching Model
|
|
||||||
UnitDisabled Model
|
|
||||||
LoopToggle Model
|
|
||||||
Mute Model
|
|
||||||
Solo Model
|
|
||||||
Oversampling Model
|
|
||||||
InstrEditor Model
|
|
||||||
InstrPresets Model
|
|
||||||
InstrComment Model
|
|
||||||
Thread1 Model
|
|
||||||
Thread2 Model
|
|
||||||
Thread3 Model
|
|
||||||
Thread4 Model
|
|
||||||
|
|
||||||
simpleBool bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func MakeBool(value BoolValue) Bool {
|
|
||||||
return Bool{value: value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func MakeBoolFromPtr(value *bool) Bool {
|
|
||||||
return Bool{value: (*simpleBool)(value)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Bool) Toggle() {
|
|
||||||
v.SetValue(!v.Value())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Bool) SetValue(value bool) {
|
|
||||||
if v.Enabled() && v.Value() != value {
|
|
||||||
v.value.SetValue(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Bool) Value() bool {
|
|
||||||
if v.value == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return v.value.Value()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Bool) Enabled() bool {
|
|
||||||
if v.value == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
e, ok := v.value.(Enabler)
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return e.Enabled()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *simpleBool) Value() bool { return bool(*v) }
|
|
||||||
func (v *simpleBool) SetValue(value bool) { *v = simpleBool(value) }
|
|
||||||
|
|
||||||
// Thread methods
|
|
||||||
|
|
||||||
func (m *Model) getThreadsBit(bit int) bool {
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
mask := m.d.Song.Patch[m.d.InstrIndex].ThreadMaskM1 + 1
|
|
||||||
return mask&(1<<bit) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) setThreadsBit(bit int, value bool) {
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer (*Model)(m).change("ThreadBitMask", PatchChange, MinorChange)()
|
|
||||||
mask := m.d.Song.Patch[m.d.InstrIndex].ThreadMaskM1 + 1
|
|
||||||
if value {
|
|
||||||
mask |= (1 << bit)
|
|
||||||
} else {
|
|
||||||
mask &^= (1 << bit)
|
|
||||||
}
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex].ThreadMaskM1 = max(mask-1, -1) // -1 has all threads disabled, we warn about that
|
|
||||||
m.warnAboutCrossThreadSends()
|
|
||||||
m.warnNoMultithreadSupport()
|
|
||||||
m.warnNoThread()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) warnAboutCrossThreadSends() {
|
|
||||||
for i, instr := range m.d.Song.Patch {
|
|
||||||
for _, unit := range instr.Units {
|
|
||||||
if unit.Type == "send" {
|
|
||||||
targetID, ok := unit.Parameters["target"]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
it, _, err := m.d.Song.Patch.FindUnit(targetID)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if instr.ThreadMaskM1 != m.d.Song.Patch[it].ThreadMaskM1 {
|
|
||||||
m.Alerts().AddNamed("CrossThreadSend", fmt.Sprintf("Instrument %d '%s' has a send to instrument %d '%s' but they are not on the same threads, which may cause issues", i+1, instr.Name, it+1, m.d.Song.Patch[it].Name), Warning)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.Alerts().ClearNamed("CrossThreadSend")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) warnNoMultithreadSupport() {
|
|
||||||
for _, instr := range m.d.Song.Patch {
|
|
||||||
if instr.ThreadMaskM1 > 0 && !m.synthers[m.syntherIndex].SupportsMultithreading() {
|
|
||||||
m.Alerts().AddNamed("NoMultithreadSupport", "The current synth does not support multithreading and the patch was configured to use more than one thread", Warning)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.Alerts().ClearNamed("NoMultithreadSupport")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) warnNoThread() {
|
|
||||||
for i, instr := range m.d.Song.Patch {
|
|
||||||
if instr.ThreadMaskM1 == -1 {
|
|
||||||
m.Alerts().AddNamed("NoThread", fmt.Sprintf("Instrument %d '%s' is not rendered on any thread", i+1, instr.Name), Warning)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.Alerts().ClearNamed("NoThread")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) Thread1() Bool { return MakeBool((*Thread1)(m)) }
|
|
||||||
func (m *Thread1) Value() bool { return (*Model)(m).getThreadsBit(0) }
|
|
||||||
func (m *Thread1) SetValue(val bool) { (*Model)(m).setThreadsBit(0, val) }
|
|
||||||
|
|
||||||
func (m *Model) Thread2() Bool { return MakeBool((*Thread2)(m)) }
|
|
||||||
func (m *Thread2) Value() bool { return (*Model)(m).getThreadsBit(1) }
|
|
||||||
func (m *Thread2) SetValue(val bool) { (*Model)(m).setThreadsBit(1, val) }
|
|
||||||
|
|
||||||
func (m *Model) Thread3() Bool { return MakeBool((*Thread3)(m)) }
|
|
||||||
func (m *Thread3) Value() bool { return (*Model)(m).getThreadsBit(2) }
|
|
||||||
func (m *Thread3) SetValue(val bool) { (*Model)(m).setThreadsBit(2, val) }
|
|
||||||
|
|
||||||
func (m *Model) Thread4() Bool { return MakeBool((*Thread4)(m)) }
|
|
||||||
func (m *Thread4) Value() bool { return (*Model)(m).getThreadsBit(3) }
|
|
||||||
func (m *Thread4) SetValue(val bool) { (*Model)(m).setThreadsBit(3, val) }
|
|
||||||
|
|
||||||
// Panic methods
|
|
||||||
|
|
||||||
func (m *Model) Panic() Bool { return MakeBool((*Panic)(m)) }
|
|
||||||
func (m *Panic) Value() bool { return m.panic }
|
|
||||||
func (m *Panic) SetValue(val bool) { (*Model)(m).setPanic(val) }
|
|
||||||
|
|
||||||
// IsRecording methods
|
|
||||||
|
|
||||||
func (m *Model) IsRecording() Bool { return MakeBool((*IsRecording)(m)) }
|
|
||||||
func (m *IsRecording) Value() bool { return (*Model)(m).recording }
|
|
||||||
func (m *IsRecording) SetValue(val bool) {
|
|
||||||
m.recording = val
|
|
||||||
m.instrEnlarged = val
|
|
||||||
TrySend(m.broker.ToPlayer, any(RecordingMsg{val}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playing methods
|
|
||||||
|
|
||||||
func (m *Model) Playing() Bool { return MakeBool((*Playing)(m)) }
|
|
||||||
func (m *Playing) Value() bool { return m.playing }
|
|
||||||
func (m *Playing) SetValue(val bool) {
|
|
||||||
m.playing = val
|
|
||||||
if m.playing {
|
|
||||||
(*Model)(m).setPanic(false)
|
|
||||||
TrySend(m.broker.ToPlayer, any(StartPlayMsg{m.d.Cursor.SongPos}))
|
|
||||||
} else {
|
|
||||||
TrySend(m.broker.ToPlayer, any(IsPlayingMsg{val}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (m *Playing) Enabled() bool { return m.playing || !m.instrEnlarged }
|
|
||||||
|
|
||||||
// InstrEnlarged methods
|
|
||||||
|
|
||||||
func (m *Model) InstrEnlarged() Bool { return MakeBoolFromPtr(&m.instrEnlarged) }
|
|
||||||
|
|
||||||
// InstrEditor methods
|
|
||||||
|
|
||||||
func (m *Model) InstrEditor() Bool { return MakeBool((*InstrEditor)(m)) }
|
|
||||||
func (m *InstrEditor) Value() bool { return m.d.InstrumentTab == InstrumentEditorTab }
|
|
||||||
func (m *InstrEditor) SetValue(val bool) {
|
|
||||||
if val {
|
|
||||||
m.d.InstrumentTab = InstrumentEditorTab
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) InstrComment() Bool { return MakeBool((*InstrComment)(m)) }
|
|
||||||
func (m *InstrComment) Value() bool { return m.d.InstrumentTab == InstrumentCommentTab }
|
|
||||||
func (m *InstrComment) SetValue(val bool) {
|
|
||||||
if val {
|
|
||||||
m.d.InstrumentTab = InstrumentCommentTab
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) InstrPresets() Bool { return MakeBool((*InstrPresets)(m)) }
|
|
||||||
func (m *InstrPresets) Value() bool { return m.d.InstrumentTab == InstrumentPresetsTab }
|
|
||||||
func (m *InstrPresets) SetValue(val bool) {
|
|
||||||
if val {
|
|
||||||
m.d.InstrumentTab = InstrumentPresetsTab
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Follow methods
|
|
||||||
|
|
||||||
func (m *Model) Follow() Bool { return MakeBoolFromPtr(&m.follow) }
|
|
||||||
|
|
||||||
// TrackMidiIn (Midi Input for notes in the tracks)
|
|
||||||
|
|
||||||
func (m *Model) TrackMidiIn() Bool { return MakeBool((*TrackMidiIn)(m)) }
|
|
||||||
func (m *TrackMidiIn) Value() bool { return m.broker.mIDIEventsToGUI.Load() }
|
|
||||||
func (m *TrackMidiIn) SetValue(val bool) { m.broker.mIDIEventsToGUI.Store(val) }
|
|
||||||
|
|
||||||
// Effect methods
|
|
||||||
|
|
||||||
func (m *Model) Effect() Bool { return MakeBool((*Effect)(m)) }
|
|
||||||
func (m *Effect) Value() bool {
|
|
||||||
if m.d.Cursor.Track < 0 || m.d.Cursor.Track >= len(m.d.Song.Score.Tracks) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return m.d.Song.Score.Tracks[m.d.Cursor.Track].Effect
|
|
||||||
}
|
|
||||||
func (m *Effect) SetValue(val bool) {
|
|
||||||
if m.d.Cursor.Track < 0 || m.d.Cursor.Track >= len(m.d.Song.Score.Tracks) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.d.Song.Score.Tracks[m.d.Cursor.Track].Effect = val
|
|
||||||
}
|
|
||||||
|
|
||||||
// Oversampling methods
|
|
||||||
|
|
||||||
func (m *Model) Oversampling() Bool { return MakeBool((*Oversampling)(m)) }
|
|
||||||
func (m *Oversampling) Value() bool { return m.oversampling }
|
|
||||||
func (m *Oversampling) SetValue(val bool) {
|
|
||||||
m.oversampling = val
|
|
||||||
TrySend(m.broker.ToDetector, MsgToDetector{HasOversampling: true, Oversampling: val})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnitSearching methods
|
|
||||||
|
|
||||||
func (m *Model) UnitSearching() Bool { return MakeBool((*UnitSearching)(m)) }
|
|
||||||
func (m *UnitSearching) Value() bool { return m.d.UnitSearching }
|
|
||||||
func (m *UnitSearching) SetValue(val bool) {
|
|
||||||
m.d.UnitSearching = val
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
m.d.UnitSearchString = ""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if m.d.UnitIndex < 0 || m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
|
|
||||||
m.d.UnitSearchString = ""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.d.UnitSearchString = m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Type
|
|
||||||
(*Model)(m).updateDerivedUnitSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnitDisabled methods
|
|
||||||
|
|
||||||
func (m *Model) UnitDisabled() Bool { return MakeBool((*UnitDisabled)(m)) }
|
|
||||||
func (m *UnitDisabled) Value() bool {
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if m.d.UnitIndex < 0 || m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Disabled
|
|
||||||
}
|
|
||||||
func (m *UnitDisabled) SetValue(val bool) {
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
l := ((*Model)(m)).Units()
|
|
||||||
r := l.listRange()
|
|
||||||
defer (*Model)(m).change("UnitDisabledSet", PatchChange, MajorChange)()
|
|
||||||
for i := r.Start; i < r.End; i++ {
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex].Units[i].Disabled = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (m *UnitDisabled) Enabled() bool {
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(m.d.Song.Patch[m.d.InstrIndex].Units) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoopToggle methods
|
|
||||||
|
|
||||||
func (m *Model) LoopToggle() Bool { return MakeBool((*LoopToggle)(m)) }
|
|
||||||
func (m *LoopToggle) Value() bool { return m.loop.Length > 0 }
|
|
||||||
func (t *LoopToggle) SetValue(val bool) {
|
|
||||||
m := (*Model)(t)
|
|
||||||
newLoop := Loop{}
|
|
||||||
if val {
|
|
||||||
l := m.OrderRows()
|
|
||||||
r := l.listRange()
|
|
||||||
newLoop = Loop{r.Start, r.End - r.Start}
|
|
||||||
}
|
|
||||||
m.setLoop(newLoop)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UniquePatterns methods
|
|
||||||
|
|
||||||
func (m *Model) UniquePatterns() Bool { return MakeBoolFromPtr(&m.uniquePatterns) }
|
|
||||||
|
|
||||||
// Mute methods
|
|
||||||
func (m *Model) Mute() Bool { return MakeBool((*Mute)(m)) }
|
|
||||||
func (m *Mute) Value() bool {
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return m.d.Song.Patch[m.d.InstrIndex].Mute
|
|
||||||
}
|
|
||||||
func (m *Mute) SetValue(val bool) {
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer (*Model)(m).change("Mute", PatchChange, MinorChange)()
|
|
||||||
a, b := min(m.d.InstrIndex, m.d.InstrIndex2), max(m.d.InstrIndex, m.d.InstrIndex2)
|
|
||||||
for i := a; i <= b; i++ {
|
|
||||||
if i < 0 || i >= len(m.d.Song.Patch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m.d.Song.Patch[i].Mute = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (m *Mute) Enabled() bool { return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch) }
|
|
||||||
|
|
||||||
// Solo methods
|
|
||||||
|
|
||||||
func (m *Model) Solo() Bool { return MakeBool((*Solo)(m)) }
|
|
||||||
func (m *Solo) Value() bool {
|
|
||||||
a, b := min(m.d.InstrIndex, m.d.InstrIndex2), max(m.d.InstrIndex, m.d.InstrIndex2)
|
|
||||||
for i := range m.d.Song.Patch {
|
|
||||||
if i < 0 || i >= len(m.d.Song.Patch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (i >= a && i <= b) == m.d.Song.Patch[i].Mute {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
func (m *Solo) SetValue(val bool) {
|
|
||||||
defer (*Model)(m).change("Solo", PatchChange, MinorChange)()
|
|
||||||
a, b := min(m.d.InstrIndex, m.d.InstrIndex2), max(m.d.InstrIndex, m.d.InstrIndex2)
|
|
||||||
for i := range m.d.Song.Patch {
|
|
||||||
if i < 0 || i >= len(m.d.Song.Patch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m.d.Song.Patch[i].Mute = !(i >= a && i <= b) && val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (m *Solo) Enabled() bool { return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch) }
|
|
||||||
|
|
||||||
// LinkInstrTrack methods
|
|
||||||
|
|
||||||
func (m *Model) LinkInstrTrack() Bool { return MakeBoolFromPtr(&m.linkInstrTrack) }
|
|
||||||
@@ -98,7 +98,7 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
MsgToSpecAn struct {
|
MsgToSpecAn struct {
|
||||||
SpecSettings SpecAnSettings
|
SpecSettings specAnSettings
|
||||||
HasSettings bool
|
HasSettings bool
|
||||||
Data any
|
Data any
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ type (
|
|||||||
patch []derivedInstrument
|
patch []derivedInstrument
|
||||||
tracks []derivedTrack
|
tracks []derivedTrack
|
||||||
railError RailError
|
railError RailError
|
||||||
presetSearch derivedPresetSearch
|
|
||||||
searchResults []string
|
searchResults []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,52 +53,6 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// public methods to access the derived data
|
|
||||||
|
|
||||||
func (s *Model) RailError() RailError { return s.derived.railError }
|
|
||||||
|
|
||||||
func (s *Model) RailWidth() int {
|
|
||||||
i := s.d.InstrIndex
|
|
||||||
if i < 0 || i >= len(s.derived.patch) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return s.derived.patch[i].railWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) Wires(yield func(wire Wire) bool) {
|
|
||||||
i := m.d.InstrIndex
|
|
||||||
if i < 0 || i >= len(m.derived.patch) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, wire := range m.derived.patch[i].wires {
|
|
||||||
wire.Highlight = (wire.FromSet && m.d.UnitIndex == wire.From) || (wire.ToSet && m.d.UnitIndex == wire.To.Y && m.d.ParamIndex == wire.To.X)
|
|
||||||
if !yield(wire) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) TrackTitle(index int) string {
|
|
||||||
if index < 0 || index >= len(m.derived.tracks) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return m.derived.tracks[index].title
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) PatternUnique(track, pat int) bool {
|
|
||||||
if track < 0 || track >= len(m.derived.tracks) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if pat < 0 || pat >= len(m.derived.tracks[track].patternUseCounts) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return m.derived.tracks[track].patternUseCounts[pat] <= 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *RailError) Error() string { return e.Err.Error() }
|
|
||||||
|
|
||||||
func (s *Rail) StackAfter() int { return s.PassThrough + s.StackUse.NumOutputs }
|
|
||||||
|
|
||||||
// init / update methods
|
// init / update methods
|
||||||
|
|
||||||
func (m *Model) updateDeriveData(changeType ChangeType) {
|
func (m *Model) updateDeriveData(changeType ChangeType) {
|
||||||
|
|||||||
@@ -7,28 +7,95 @@ import (
|
|||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const MAX_INTEGRATED_DATA = 10 * 60 * 60 // 1 hour of samples at 10 Hz (100 ms per sample)
|
||||||
|
// In the detector, we clamp the signal levels to +-MAX_SIGNAL_AMPLITUDE to
|
||||||
|
// avoid Inf results. This is 240 dBFS. max float32 is about 3.4e38, so squaring
|
||||||
|
// the amplitude values gives 1e24, and adding 4410 of those together (when
|
||||||
|
// taking the mean) gives a value < 1e37, which is still < max float32.
|
||||||
|
const MAX_SIGNAL_AMPLITUDE = 1e12
|
||||||
|
|
||||||
|
// Detector returns a DetectorModel which provides access to the detector
|
||||||
|
// settings and results.
|
||||||
|
func (m *Model) Detector() *DetectorModel { return (*DetectorModel)(m) }
|
||||||
|
|
||||||
|
type DetectorModel Model
|
||||||
|
|
||||||
|
// Result returns the latest DetectorResult from the detector.
|
||||||
|
func (m *DetectorModel) Result() DetectorResult { return m.detectorResult }
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Detector struct {
|
DetectorResult struct {
|
||||||
|
Loudness LoudnessResult
|
||||||
|
Peaks PeakResult
|
||||||
|
}
|
||||||
|
LoudnessResult [NumLoudnessTypes]Decibel
|
||||||
|
PeakResult [NumPeakTypes][2]Decibel
|
||||||
|
Decibel float32
|
||||||
|
LoudnessType int
|
||||||
|
PeakType int
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LoudnessMomentary LoudnessType = iota
|
||||||
|
LoudnessShortTerm
|
||||||
|
LoudnessMaxMomentary
|
||||||
|
LoudnessMaxShortTerm
|
||||||
|
LoudnessIntegrated
|
||||||
|
NumLoudnessTypes
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PeakMomentary PeakType = iota
|
||||||
|
PeakShortTerm
|
||||||
|
PeakIntegrated
|
||||||
|
NumPeakTypes
|
||||||
|
)
|
||||||
|
|
||||||
|
// Weighting returns an Int property for setting the detector weighting type.
|
||||||
|
func (m *DetectorModel) Weighting() Int { return MakeInt((*detectorWeighting)(m)) }
|
||||||
|
|
||||||
|
type detectorWeighting Model
|
||||||
|
|
||||||
|
func (v *detectorWeighting) Value() int { return int(v.weightingType) }
|
||||||
|
func (v *detectorWeighting) SetValue(value int) bool {
|
||||||
|
v.weightingType = WeightingType(value)
|
||||||
|
TrySend(v.broker.ToDetector, MsgToDetector{HasWeightingType: true, WeightingType: WeightingType(value)})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (v *detectorWeighting) Range() RangeInclusive {
|
||||||
|
return RangeInclusive{0, int(NumWeightingTypes) - 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeightingType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
KWeighting WeightingType = iota
|
||||||
|
AWeighting
|
||||||
|
CWeighting
|
||||||
|
NoWeighting
|
||||||
|
NumWeightingTypes
|
||||||
|
)
|
||||||
|
|
||||||
|
// Oversampling returns a Bool property for setting whether the peak detector
|
||||||
|
// uses oversampling to calculate true peaks, or just sample peaks if not.
|
||||||
|
func (m *DetectorModel) Oversampling() Bool { return MakeBool((*detectorOversampling)(m)) }
|
||||||
|
|
||||||
|
type detectorOversampling Model
|
||||||
|
|
||||||
|
func (m *detectorOversampling) Value() bool { return m.oversampling }
|
||||||
|
func (m *detectorOversampling) SetValue(val bool) {
|
||||||
|
m.oversampling = val
|
||||||
|
TrySend(m.broker.ToDetector, MsgToDetector{HasOversampling: true, Oversampling: val})
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
detector struct {
|
||||||
broker *Broker
|
broker *Broker
|
||||||
loudnessDetector loudnessDetector
|
loudnessDetector loudnessDetector
|
||||||
peakDetector peakDetector
|
peakDetector peakDetector
|
||||||
chunker chunker
|
chunker chunker
|
||||||
}
|
}
|
||||||
|
|
||||||
WeightingType int
|
|
||||||
LoudnessType int
|
|
||||||
PeakType int
|
|
||||||
|
|
||||||
Decibel float32
|
|
||||||
|
|
||||||
LoudnessResult [NumLoudnessTypes]Decibel
|
|
||||||
PeakResult [NumPeakTypes][2]Decibel
|
|
||||||
|
|
||||||
DetectorResult struct {
|
|
||||||
Loudness LoudnessResult
|
|
||||||
Peaks PeakResult
|
|
||||||
}
|
|
||||||
|
|
||||||
loudnessDetector struct {
|
loudnessDetector struct {
|
||||||
weighting weighting
|
weighting weighting
|
||||||
states [2][3]biquadState
|
states [2][3]biquadState
|
||||||
@@ -62,52 +129,14 @@ type (
|
|||||||
history [11]float32
|
history [11]float32
|
||||||
tmp, tmp2 []float32
|
tmp, tmp2 []float32
|
||||||
}
|
}
|
||||||
|
|
||||||
chunker struct {
|
|
||||||
buffer sointu.AudioBuffer
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func runDetector(b *Broker) {
|
||||||
LoudnessMomentary LoudnessType = iota
|
s := &detector{
|
||||||
LoudnessShortTerm
|
|
||||||
LoudnessMaxMomentary
|
|
||||||
LoudnessMaxShortTerm
|
|
||||||
LoudnessIntegrated
|
|
||||||
NumLoudnessTypes
|
|
||||||
)
|
|
||||||
|
|
||||||
const MAX_INTEGRATED_DATA = 10 * 60 * 60 // 1 hour of samples at 10 Hz (100 ms per sample)
|
|
||||||
// In the detector, we clamp the signal levels to +-MAX_SIGNAL_AMPLITUDE to
|
|
||||||
// avoid Inf results. This is 240 dBFS. max float32 is about 3.4e38, so squaring
|
|
||||||
// the amplitude values gives 1e24, and adding 4410 of those together (when
|
|
||||||
// taking the mean) gives a value < 1e37, which is still < max float32.
|
|
||||||
const MAX_SIGNAL_AMPLITUDE = 1e12
|
|
||||||
|
|
||||||
const (
|
|
||||||
PeakMomentary PeakType = iota
|
|
||||||
PeakShortTerm
|
|
||||||
PeakIntegrated
|
|
||||||
NumPeakTypes
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
KWeighting WeightingType = iota
|
|
||||||
AWeighting
|
|
||||||
CWeighting
|
|
||||||
NoWeighting
|
|
||||||
NumWeightingTypes
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewDetector(b *Broker) *Detector {
|
|
||||||
return &Detector{
|
|
||||||
broker: b,
|
broker: b,
|
||||||
loudnessDetector: makeLoudnessDetector(KWeighting),
|
loudnessDetector: makeLoudnessDetector(KWeighting),
|
||||||
peakDetector: makePeakDetector(true),
|
peakDetector: makePeakDetector(true),
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Detector) Run() {
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-s.broker.CloseDetector:
|
case <-s.broker.CloseDetector:
|
||||||
@@ -119,7 +148,7 @@ func (s *Detector) Run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Detector) handleMsg(msg MsgToDetector) {
|
func (s *detector) handleMsg(msg MsgToDetector) {
|
||||||
if msg.Reset {
|
if msg.Reset {
|
||||||
s.loudnessDetector.reset()
|
s.loudnessDetector.reset()
|
||||||
s.peakDetector.reset()
|
s.peakDetector.reset()
|
||||||
@@ -419,6 +448,17 @@ func (d *peakDetector) reset() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chunker maintains a buffer of audio data. Its Process method appends an input
|
||||||
|
// buffer to the buffer and calls a callback function with chunks of specified
|
||||||
|
// length and overlap. The remaining data is kept in the buffer for the next
|
||||||
|
// call.
|
||||||
|
type chunker struct {
|
||||||
|
buffer sointu.AudioBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process appends input to the internal buffer and calls cb with chunks of
|
||||||
|
// windowLen length and overlap overlap. The remaining data is kept in the
|
||||||
|
// internal buffer.
|
||||||
func (c *chunker) Process(input sointu.AudioBuffer, windowLen, overlap int, cb func(sointu.AudioBuffer)) {
|
func (c *chunker) Process(input sointu.AudioBuffer, windowLen, overlap int, cb func(sointu.AudioBuffer)) {
|
||||||
c.buffer = append(c.buffer, input...)
|
c.buffer = append(c.buffer, input...)
|
||||||
b := c.buffer
|
b := c.buffer
|
||||||
|
|||||||
@@ -1,4 +1,23 @@
|
|||||||
/*
|
/*
|
||||||
Package tracker contains the data model for the Sointu tracker GUI.
|
Package tracker contains the data model for the Sointu tracker GUI.
|
||||||
|
|
||||||
|
The tracker package defines the Model struct, which holds the entire application
|
||||||
|
state, including the song data, instruments, effects, and large part of the UI
|
||||||
|
state.
|
||||||
|
|
||||||
|
The GUI does not modify the Model data directly, rather, there are types Action,
|
||||||
|
Bool, Int, String, List and Table which can be used to manipulate the model data
|
||||||
|
in a controlled way. For example, model.ShowLicense() returns an Action to show
|
||||||
|
the license to the user, which can be executed with model.ShowLicense().Do().
|
||||||
|
|
||||||
|
The various Actions and other data manipulation methods are grouped based on
|
||||||
|
their functionalities. For example, model.Instrument() groups all the ways to
|
||||||
|
manipulate the instrument(s). Similarly, model.Play() groups all the ways to
|
||||||
|
start and stop playback.
|
||||||
|
|
||||||
|
The method naming aims at API fluency. For example, model.Play().FromBeginning()
|
||||||
|
returns an Action to start playing the song from the beginning. Similarly,
|
||||||
|
model.Instrument().Add() returns an Action to add a new instrument to the song
|
||||||
|
and model.Instrument().List() returns a List of all the instruments.
|
||||||
*/
|
*/
|
||||||
package tracker
|
package tracker
|
||||||
|
|||||||
190
tracker/files.go
190
tracker/files.go
@@ -1,190 +0,0 @@
|
|||||||
package tracker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
|
||||||
"github.com/vsariola/sointu/vm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *Model) ReadSong(r io.ReadCloser) {
|
|
||||||
b, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = r.Close()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var song sointu.Song
|
|
||||||
if errJSON := json.Unmarshal(b, &song); errJSON != nil {
|
|
||||||
if errYaml := yaml.Unmarshal(b, &song); errYaml != nil {
|
|
||||||
m.Alerts().Add(fmt.Sprintf("Error unmarshaling a song file: %v / %v", errYaml, errJSON), Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f := m.change("LoadSong", SongChange, MajorChange)
|
|
||||||
m.d.Song = song
|
|
||||||
if f, ok := r.(*os.File); ok {
|
|
||||||
m.d.FilePath = f.Name()
|
|
||||||
// when the song is loaded from a file, we are quite confident that the file is persisted and thus
|
|
||||||
// we can close sointu without worrying about losing changes
|
|
||||||
m.d.ChangedSinceSave = false
|
|
||||||
}
|
|
||||||
f()
|
|
||||||
m.completeAction(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) WriteSong(w io.WriteCloser) {
|
|
||||||
path := ""
|
|
||||||
var extension = filepath.Ext(path)
|
|
||||||
var contents []byte
|
|
||||||
var err error
|
|
||||||
if extension == ".json" {
|
|
||||||
contents, err = json.Marshal(m.d.Song)
|
|
||||||
} else {
|
|
||||||
contents, err = yaml.Marshal(m.d.Song)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
m.Alerts().Add(fmt.Sprintf("Error marshaling a song file: %v", err), Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err := w.Write(contents); err != nil {
|
|
||||||
m.Alerts().Add(fmt.Sprintf("Error writing to file: %v", err), Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if f, ok := w.(*os.File); ok {
|
|
||||||
path = f.Name()
|
|
||||||
// when the song is saved to a file, we are quite confident that the file is persisted and thus
|
|
||||||
// we can close sointu without worrying about losing changes
|
|
||||||
m.d.ChangedSinceSave = false
|
|
||||||
}
|
|
||||||
if err := w.Close(); err != nil {
|
|
||||||
m.Alerts().Add(fmt.Sprintf("Error rendering the song during export: %v", err), Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.d.FilePath = path
|
|
||||||
m.completeAction(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) WriteWav(w io.WriteCloser, pcm16 bool) {
|
|
||||||
m.dialog = NoDialog
|
|
||||||
song := m.d.Song.Copy()
|
|
||||||
go func() {
|
|
||||||
b := make([]byte, 32+2)
|
|
||||||
rand.Read(b)
|
|
||||||
name := fmt.Sprintf("%x", b)[2 : 32+2]
|
|
||||||
data, err := sointu.Play(m.synthers[m.syntherIndex], song, func(p float32) {
|
|
||||||
txt := fmt.Sprintf("Exporting song: %.0f%%", p*100)
|
|
||||||
TrySend(m.broker.ToModel, MsgToModel{Data: Alert{Message: txt, Priority: Info, Name: name, Duration: defaultAlertDuration}})
|
|
||||||
}) // render the song to calculate its length
|
|
||||||
if err != nil {
|
|
||||||
txt := fmt.Sprintf("Error rendering the song during export: %v", err)
|
|
||||||
TrySend(m.broker.ToModel, MsgToModel{Data: Alert{Message: txt, Priority: Error, Name: name, Duration: defaultAlertDuration}})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
buffer, err := data.Wav(pcm16)
|
|
||||||
if err != nil {
|
|
||||||
txt := fmt.Sprintf("Error converting to .wav: %v", err)
|
|
||||||
TrySend(m.broker.ToModel, MsgToModel{Data: Alert{Message: txt, Priority: Error, Name: name, Duration: defaultAlertDuration}})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(buffer)
|
|
||||||
w.Close()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SaveInstrument(w io.WriteCloser) bool {
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
m.Alerts().Add("No instrument selected", Error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
path := ""
|
|
||||||
if f, ok := w.(*os.File); ok {
|
|
||||||
path = f.Name()
|
|
||||||
}
|
|
||||||
var extension = filepath.Ext(path)
|
|
||||||
var contents []byte
|
|
||||||
var err error
|
|
||||||
instr := m.d.Song.Patch[m.d.InstrIndex]
|
|
||||||
if _, ok := w.(*os.File); ok {
|
|
||||||
instr.Name = "" // don't save the instrument name to a file; we'll replace the instruments name with the filename when loading from a file
|
|
||||||
}
|
|
||||||
if extension == ".json" {
|
|
||||||
contents, err = json.Marshal(instr)
|
|
||||||
} else {
|
|
||||||
contents, err = yaml.Marshal(instr)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
m.Alerts().Add(fmt.Sprintf("Error marshaling an instrument file: %v", err), Error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
w.Write(contents)
|
|
||||||
w.Close()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) LoadInstrument(r io.ReadCloser) bool {
|
|
||||||
if m.d.InstrIndex < 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
b, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
r.Close() // if we can't close the file, it's not a big deal, so ignore the error
|
|
||||||
var instrument sointu.Instrument
|
|
||||||
var errJSON, errYaml, err4ki, err4kp error
|
|
||||||
var patch sointu.Patch
|
|
||||||
errJSON = json.Unmarshal(b, &instrument)
|
|
||||||
if errJSON == nil {
|
|
||||||
goto success
|
|
||||||
}
|
|
||||||
errYaml = yaml.Unmarshal(b, &instrument)
|
|
||||||
if errYaml == nil {
|
|
||||||
goto success
|
|
||||||
}
|
|
||||||
patch, err4kp = sointu.Read4klangPatch(bytes.NewReader(b))
|
|
||||||
if err4kp == nil {
|
|
||||||
defer m.change("LoadInstrument", PatchChange, MajorChange)()
|
|
||||||
m.d.Song.Patch = patch
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
instrument, err4ki = sointu.Read4klangInstrument(bytes.NewReader(b))
|
|
||||||
if err4ki == nil {
|
|
||||||
goto success
|
|
||||||
}
|
|
||||||
m.Alerts().Add(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v / %v / %v", errYaml, errJSON, err4ki, err4kp), Error)
|
|
||||||
return false
|
|
||||||
success:
|
|
||||||
if f, ok := r.(*os.File); ok {
|
|
||||||
filename := f.Name()
|
|
||||||
// the instrument names are generally junk, replace them with the filename without extension
|
|
||||||
instrument.Name = filepath.Base(filename[:len(filename)-len(filepath.Ext(filename))])
|
|
||||||
}
|
|
||||||
defer m.change("LoadInstrument", PatchChange, MajorChange)()
|
|
||||||
for len(m.d.Song.Patch) <= m.d.InstrIndex {
|
|
||||||
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
|
|
||||||
}
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex] = sointu.Instrument{}
|
|
||||||
numVoices := m.d.Song.Patch.NumVoices()
|
|
||||||
if numVoices >= vm.MAX_VOICES {
|
|
||||||
// this really shouldn't happen, as we have already cleared the
|
|
||||||
// instrument and assuming each instrument has at least 1 voice, it
|
|
||||||
// should have freed up some voices
|
|
||||||
m.Alerts().Add(fmt.Sprintf("The patch has already %d voices", vm.MAX_VOICES), Error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
instrument.NumVoices = clamp(instrument.NumVoices, 1, 32-numVoices)
|
|
||||||
m.assignUnitIDs(instrument.Units)
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex] = instrument
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -53,7 +53,7 @@ type (
|
|||||||
|
|
||||||
func NewInstrumentEditor(m *tracker.Model) *InstrumentEditor {
|
func NewInstrumentEditor(m *tracker.Model) *InstrumentEditor {
|
||||||
ret := &InstrumentEditor{
|
ret := &InstrumentEditor{
|
||||||
dragList: NewDragList(m.Units(), layout.Vertical),
|
dragList: NewDragList(m.Unit().List(), layout.Vertical),
|
||||||
addUnitBtn: new(Clickable),
|
addUnitBtn: new(Clickable),
|
||||||
searchEditor: NewEditor(true, true, text.Start),
|
searchEditor: NewEditor(true, true, text.Start),
|
||||||
DeleteUnitBtn: new(Clickable),
|
DeleteUnitBtn: new(Clickable),
|
||||||
@@ -62,9 +62,9 @@ func NewInstrumentEditor(m *tracker.Model) *InstrumentEditor {
|
|||||||
CopyUnitBtn: new(Clickable),
|
CopyUnitBtn: new(Clickable),
|
||||||
SelectTypeBtn: new(Clickable),
|
SelectTypeBtn: new(Clickable),
|
||||||
commentEditor: NewEditor(true, true, text.Start),
|
commentEditor: NewEditor(true, true, text.Start),
|
||||||
paramTable: NewScrollTable(m.Params().Table(), m.ParamVertList().List(), m.Units()),
|
paramTable: NewScrollTable(m.Params().Table(), m.Params().Columns(), m.Unit().List()),
|
||||||
searchList: NewDragList(m.SearchResults(), layout.Vertical),
|
searchList: NewDragList(m.Unit().SearchResults(), layout.Vertical),
|
||||||
searching: m.UnitSearching(),
|
searching: m.Unit().Searching(),
|
||||||
}
|
}
|
||||||
ret.caser = cases.Title(language.English)
|
ret.caser = cases.Title(language.English)
|
||||||
ret.copyHint = makeHint("Copy unit", " (%s)", "Copy")
|
ret.copyHint = makeHint("Copy unit", " (%s)", "Copy")
|
||||||
@@ -95,9 +95,9 @@ func (ul *InstrumentEditor) layoutList(gtx C) D {
|
|||||||
element := func(gtx C, i int) D {
|
element := func(gtx C, i int) D {
|
||||||
gtx.Constraints.Max.Y = gtx.Dp(20)
|
gtx.Constraints.Max.Y = gtx.Dp(20)
|
||||||
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||||
u := t.Unit(i)
|
u := t.Unit().Item(i)
|
||||||
editorStyle := t.Theme.InstrumentEditor.UnitList.Name
|
editorStyle := t.Theme.InstrumentEditor.UnitList.Name
|
||||||
signalError := t.RailError()
|
signalError := t.Unit().RailError()
|
||||||
switch {
|
switch {
|
||||||
case u.Disabled:
|
case u.Disabled:
|
||||||
editorStyle = t.Theme.InstrumentEditor.UnitList.NameDisabled
|
editorStyle = t.Theme.InstrumentEditor.UnitList.NameDisabled
|
||||||
@@ -107,7 +107,7 @@ func (ul *InstrumentEditor) layoutList(gtx C) D {
|
|||||||
unitName := func(gtx C) D {
|
unitName := func(gtx C) D {
|
||||||
if i == ul.dragList.TrackerList.Selected() {
|
if i == ul.dragList.TrackerList.Selected() {
|
||||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||||
return ul.searchEditor.Layout(gtx, t.Model.UnitSearch(), t.Theme, &editorStyle, "---")
|
return ul.searchEditor.Layout(gtx, t.Model.Unit().SearchTerm(), t.Theme, &editorStyle, "---")
|
||||||
} else {
|
} else {
|
||||||
text := u.Type
|
text := u.Type
|
||||||
if text == "" {
|
if text == "" {
|
||||||
@@ -169,40 +169,40 @@ func (ul *InstrumentEditor) update(gtx C) {
|
|||||||
case key.NameRightArrow:
|
case key.NameRightArrow:
|
||||||
t.PatchPanel.instrEditor.paramTable.RowTitleList.Focus()
|
t.PatchPanel.instrEditor.paramTable.RowTitleList.Focus()
|
||||||
case key.NameDeleteBackward:
|
case key.NameDeleteBackward:
|
||||||
t.SetSelectedUnitType("")
|
t.Unit().SetType("")
|
||||||
t.UnitSearching().SetValue(true)
|
t.Unit().Searching().SetValue(true)
|
||||||
ul.searchEditor.Focus()
|
ul.searchEditor.Focus()
|
||||||
case key.NameEnter, key.NameReturn:
|
case key.NameEnter, key.NameReturn:
|
||||||
t.Model.AddUnit(e.Modifiers.Contain(key.ModCtrl)).Do()
|
t.Model.Unit().Add(e.Modifiers.Contain(key.ModCtrl)).Do()
|
||||||
t.UnitSearching().SetValue(true)
|
t.Unit().Searching().SetValue(true)
|
||||||
ul.searchEditor.Focus()
|
ul.searchEditor.Focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
str := t.Model.UnitSearch()
|
str := t.Model.Unit().SearchTerm()
|
||||||
for ev := ul.searchEditor.Update(gtx, str); ev != EditorEventNone; ev = ul.searchEditor.Update(gtx, str) {
|
for ev := ul.searchEditor.Update(gtx, str); ev != EditorEventNone; ev = ul.searchEditor.Update(gtx, str) {
|
||||||
if ev == EditorEventSubmit {
|
if ev == EditorEventSubmit {
|
||||||
if str.Value() != "" {
|
if str.Value() != "" {
|
||||||
for _, n := range sointu.UnitNames {
|
for _, n := range sointu.UnitNames {
|
||||||
if strings.HasPrefix(n, str.Value()) {
|
if strings.HasPrefix(n, str.Value()) {
|
||||||
t.SetSelectedUnitType(n)
|
t.Unit().SetType(n)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.SetSelectedUnitType("")
|
t.Unit().SetType("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ul.dragList.Focus()
|
ul.dragList.Focus()
|
||||||
t.UnitSearching().SetValue(false)
|
t.Unit().Searching().SetValue(false)
|
||||||
}
|
}
|
||||||
for ul.addUnitBtn.Clicked(gtx) {
|
for ul.addUnitBtn.Clicked(gtx) {
|
||||||
t.AddUnit(false).Do()
|
t.Unit().Add(false).Do()
|
||||||
t.UnitSearching().SetValue(true)
|
t.Unit().Searching().SetValue(true)
|
||||||
ul.searchEditor.Focus()
|
ul.searchEditor.Focus()
|
||||||
}
|
}
|
||||||
for ul.CopyUnitBtn.Clicked(gtx) {
|
for ul.CopyUnitBtn.Clicked(gtx) {
|
||||||
if contents, ok := t.Units().CopyElements(); ok {
|
if contents, ok := t.Unit().List().CopyElements(); ok {
|
||||||
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))})
|
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))})
|
||||||
t.Alerts().Add("Unit(s) copied to clipboard", tracker.Info)
|
t.Alerts().Add("Unit(s) copied to clipboard", tracker.Info)
|
||||||
}
|
}
|
||||||
@@ -211,9 +211,9 @@ func (ul *InstrumentEditor) update(gtx C) {
|
|||||||
ul.ChooseUnitType(t)
|
ul.ChooseUnitType(t)
|
||||||
}
|
}
|
||||||
for ul.ClearUnitBtn.Clicked(gtx) {
|
for ul.ClearUnitBtn.Clicked(gtx) {
|
||||||
t.ClearUnit().Do()
|
t.Unit().Clear().Do()
|
||||||
t.UnitSearch().SetValue("")
|
t.Unit().SearchTerm().SetValue("")
|
||||||
t.UnitSearching().SetValue(true)
|
t.Unit().Searching().SetValue(true)
|
||||||
ul.searchList.Focus()
|
ul.searchList.Focus()
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
@@ -228,7 +228,7 @@ func (ul *InstrumentEditor) update(gtx C) {
|
|||||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||||
switch e.Name {
|
switch e.Name {
|
||||||
case key.NameEscape:
|
case key.NameEscape:
|
||||||
t.UnitSearching().SetValue(false)
|
t.Unit().Searching().SetValue(false)
|
||||||
ul.paramTable.RowTitleList.Focus()
|
ul.paramTable.RowTitleList.Focus()
|
||||||
case key.NameEnter, key.NameReturn:
|
case key.NameEnter, key.NameReturn:
|
||||||
ul.ChooseUnitType(t)
|
ul.ChooseUnitType(t)
|
||||||
@@ -288,8 +288,8 @@ func (pe *InstrumentEditor) layoutTable(gtx C) D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pe *InstrumentEditor) ChooseUnitType(t *Tracker) {
|
func (pe *InstrumentEditor) ChooseUnitType(t *Tracker) {
|
||||||
if ut, ok := t.SearchResult(pe.searchList.TrackerList.Selected()); ok {
|
if ut, ok := t.Unit().SearchResult(pe.searchList.TrackerList.Selected()); ok {
|
||||||
t.SetSelectedUnitType(ut)
|
t.Unit().SetType(ut)
|
||||||
pe.paramTable.RowTitleList.Focus()
|
pe.paramTable.RowTitleList.Focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,9 +305,9 @@ func (pe *InstrumentEditor) layoutRack(gtx C) D {
|
|||||||
cellWidth := gtx.Dp(t.Theme.UnitEditor.Width)
|
cellWidth := gtx.Dp(t.Theme.UnitEditor.Width)
|
||||||
cellHeight := gtx.Dp(t.Theme.UnitEditor.Height)
|
cellHeight := gtx.Dp(t.Theme.UnitEditor.Height)
|
||||||
rowTitleLabelWidth := gtx.Dp(t.Theme.UnitEditor.UnitList.LabelWidth)
|
rowTitleLabelWidth := gtx.Dp(t.Theme.UnitEditor.UnitList.LabelWidth)
|
||||||
rowTitleSignalWidth := gtx.Dp(t.Theme.SignalRail.SignalWidth) * t.RailWidth()
|
rowTitleSignalWidth := gtx.Dp(t.Theme.SignalRail.SignalWidth) * t.Unit().RailWidth()
|
||||||
rowTitleWidth := rowTitleLabelWidth + rowTitleSignalWidth
|
rowTitleWidth := rowTitleLabelWidth + rowTitleSignalWidth
|
||||||
signalError := t.RailError()
|
signalError := t.Unit().RailError()
|
||||||
columnTitleHeight := gtx.Dp(0)
|
columnTitleHeight := gtx.Dp(0)
|
||||||
for i := range pe.Parameters {
|
for i := range pe.Parameters {
|
||||||
for len(pe.Parameters[i]) < width {
|
for len(pe.Parameters[i]) < width {
|
||||||
@@ -321,7 +321,7 @@ func (pe *InstrumentEditor) layoutRack(gtx C) D {
|
|||||||
if y < 0 || y >= len(pe.Parameters) {
|
if y < 0 || y >= len(pe.Parameters) {
|
||||||
return D{}
|
return D{}
|
||||||
}
|
}
|
||||||
item := t.Unit(y)
|
item := t.Unit().Item(y)
|
||||||
sr := Rail(t.Theme, item.Signals)
|
sr := Rail(t.Theme, item.Signals)
|
||||||
label := Label(t.Theme, &t.Theme.UnitEditor.UnitList.Name, item.Type)
|
label := Label(t.Theme, &t.Theme.UnitEditor.UnitList.Name, item.Type)
|
||||||
switch {
|
switch {
|
||||||
@@ -360,20 +360,20 @@ func (pe *InstrumentEditor) layoutRack(gtx C) D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
param := t.Model.Params().Item(point)
|
param := t.Model.Params().Item(point)
|
||||||
paramStyle := Param(param, t.Theme, pe.Parameters[y][x], pe.paramTable.Table.Cursor() == point, t.Unit(y).Disabled)
|
paramStyle := Param(param, t.Theme, pe.Parameters[y][x], pe.paramTable.Table.Cursor() == point, t.Unit().Item(y).Disabled)
|
||||||
paramStyle.Layout(gtx)
|
paramStyle.Layout(gtx)
|
||||||
if x == t.Model.Params().RowWidth(y) {
|
if x == t.Model.Params().RowWidth(y) {
|
||||||
if y == cursor.Y {
|
if y == cursor.Y {
|
||||||
return layout.W.Layout(gtx, func(gtx C) D {
|
return layout.W.Layout(gtx, func(gtx C) D {
|
||||||
for pe.commentEditor.Update(gtx, t.UnitComment()) != EditorEventNone {
|
for pe.commentEditor.Update(gtx, t.Unit().Comment()) != EditorEventNone {
|
||||||
t.FocusPrev(gtx, false)
|
t.FocusPrev(gtx, false)
|
||||||
}
|
}
|
||||||
gtx.Constraints.Max.X = 1e6
|
gtx.Constraints.Max.X = 1e6
|
||||||
gtx.Constraints.Min.Y = 0
|
gtx.Constraints.Min.Y = 0
|
||||||
return pe.commentEditor.Layout(gtx, t.UnitComment(), t.Theme, &t.Theme.InstrumentEditor.UnitComment, "---")
|
return pe.commentEditor.Layout(gtx, t.Unit().Comment(), t.Theme, &t.Theme.InstrumentEditor.UnitComment, "---")
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
comment := t.Unit(y).Comment
|
comment := t.Unit().Item(y).Comment
|
||||||
if comment != "" {
|
if comment != "" {
|
||||||
style := t.Theme.InstrumentEditor.UnitComment.AsLabelStyle()
|
style := t.Theme.InstrumentEditor.UnitComment.AsLabelStyle()
|
||||||
label := Label(t.Theme, &style, comment)
|
label := Label(t.Theme, &style, comment)
|
||||||
@@ -408,7 +408,7 @@ func (pe *InstrumentEditor) drawSignals(gtx C, rowTitleWidth int) {
|
|||||||
gtx.Constraints.Max = gtx.Constraints.Max.Sub(p)
|
gtx.Constraints.Max = gtx.Constraints.Max.Sub(p)
|
||||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
|
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
|
||||||
defer op.Offset(image.Pt(-colP.Offset, -rowP.Offset)).Push(gtx.Ops).Pop()
|
defer op.Offset(image.Pt(-colP.Offset, -rowP.Offset)).Push(gtx.Ops).Pop()
|
||||||
for wire := range t.Wires {
|
for wire := range t.Params().Wires {
|
||||||
clr := t.Theme.UnitEditor.WireColor
|
clr := t.Theme.UnitEditor.WireColor
|
||||||
if wire.Highlight {
|
if wire.Highlight {
|
||||||
clr = t.Theme.UnitEditor.WireHighlight
|
clr = t.Theme.UnitEditor.WireHighlight
|
||||||
@@ -516,9 +516,9 @@ func mulVec(a, b f32.Point) f32.Point {
|
|||||||
|
|
||||||
func (pe *InstrumentEditor) layoutFooter(gtx C) D {
|
func (pe *InstrumentEditor) layoutFooter(gtx C) D {
|
||||||
t := TrackerFromContext(gtx)
|
t := TrackerFromContext(gtx)
|
||||||
deleteUnitBtn := ActionIconBtn(t.DeleteUnit(), t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)")
|
deleteUnitBtn := ActionIconBtn(t.Unit().Delete(), t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)")
|
||||||
copyUnitBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, pe.CopyUnitBtn, icons.ContentContentCopy, pe.copyHint)
|
copyUnitBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, pe.CopyUnitBtn, icons.ContentContentCopy, pe.copyHint)
|
||||||
disableUnitBtn := ToggleIconBtn(t.UnitDisabled(), t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, pe.disableUnitHint, pe.enableUnitHint)
|
disableUnitBtn := ToggleIconBtn(t.Unit().Disabled(), t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, pe.disableUnitHint, pe.enableUnitHint)
|
||||||
clearUnitBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, pe.ClearUnitBtn, icons.ContentClear, "Clear unit")
|
clearUnitBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, pe.ClearUnitBtn, icons.ContentClear, "Clear unit")
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Rigid(deleteUnitBtn.Layout),
|
layout.Rigid(deleteUnitBtn.Layout),
|
||||||
@@ -531,7 +531,7 @@ func (pe *InstrumentEditor) layoutFooter(gtx C) D {
|
|||||||
func (pe *InstrumentEditor) layoutUnitTypeChooser(gtx C) D {
|
func (pe *InstrumentEditor) layoutUnitTypeChooser(gtx C) D {
|
||||||
t := TrackerFromContext(gtx)
|
t := TrackerFromContext(gtx)
|
||||||
element := func(gtx C, i int) D {
|
element := func(gtx C, i int) D {
|
||||||
name, _ := t.SearchResult(i)
|
name, _ := t.Unit().SearchResult(i)
|
||||||
w := Label(t.Theme, &t.Theme.UnitEditor.Chooser, name)
|
w := Label(t.Theme, &t.Theme.UnitEditor.Chooser, name)
|
||||||
if i == pe.searchList.TrackerList.Selected() {
|
if i == pe.searchList.TrackerList.Selected() {
|
||||||
return pe.SelectTypeBtn.Layout(gtx, w.Layout)
|
return pe.SelectTypeBtn.Layout(gtx, w.Layout)
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ func NewInstrumentPresets(m *tracker.Model) *InstrumentPresets {
|
|||||||
builtinPresetsBtn: new(Clickable),
|
builtinPresetsBtn: new(Clickable),
|
||||||
saveUserPreset: new(Clickable),
|
saveUserPreset: new(Clickable),
|
||||||
deleteUserPreset: new(Clickable),
|
deleteUserPreset: new(Clickable),
|
||||||
dirList: NewDragList(m.PresetDirList().List(), layout.Vertical),
|
dirList: NewDragList(m.Preset().DirList(), layout.Vertical),
|
||||||
resultList: NewDragList(m.PresetResultList().List(), layout.Vertical),
|
resultList: NewDragList(m.Preset().SearchResultList(), layout.Vertical),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,13 +88,13 @@ func (ip *InstrumentPresets) layout(gtx C) D {
|
|||||||
ip.update(gtx)
|
ip.update(gtx)
|
||||||
// get tracker from values
|
// get tracker from values
|
||||||
tr := TrackerFromContext(gtx)
|
tr := TrackerFromContext(gtx)
|
||||||
gmDlsBtn := ToggleBtn(tr.NoGmDls(), tr.Theme, ip.gmDlsBtn, "No gm.dls", "Exclude presets using gm.dls")
|
gmDlsBtn := ToggleBtn(tr.Preset().NoGmDls(), tr.Theme, ip.gmDlsBtn, "No gm.dls", "Exclude presets using gm.dls")
|
||||||
userPresetsFilterBtn := ToggleBtn(tr.UserPresetFilter(), tr.Theme, ip.userPresetsBtn, "User", "Show only user presets")
|
userPresetsFilterBtn := ToggleBtn(tr.Preset().UserFilter(), tr.Theme, ip.userPresetsBtn, "User", "Show only user presets")
|
||||||
builtinPresetsFilterBtn := ToggleBtn(tr.BuiltinPresetsFilter(), tr.Theme, ip.builtinPresetsBtn, "Builtin", "Show only builtin presets")
|
builtinPresetsFilterBtn := ToggleBtn(tr.Preset().BuiltinFilter(), tr.Theme, ip.builtinPresetsBtn, "Builtin", "Show only builtin presets")
|
||||||
saveUserPresetBtn := ActionIconBtn(tr.SaveAsUserPreset(), tr.Theme, ip.saveUserPreset, icons.ContentSave, "Save instrument as user preset")
|
saveUserPresetBtn := ActionIconBtn(tr.Preset().Save(), tr.Theme, ip.saveUserPreset, icons.ContentSave, "Save instrument as user preset")
|
||||||
deleteUserPresetBtn := ActionIconBtn(tr.TryDeleteUserPreset(), tr.Theme, ip.deleteUserPreset, icons.ActionDelete, "Delete user preset")
|
deleteUserPresetBtn := ActionIconBtn(tr.Preset().Delete(), tr.Theme, ip.deleteUserPreset, icons.ActionDelete, "Delete user preset")
|
||||||
dirElem := func(gtx C, i int) D {
|
dirElem := func(gtx C, i int) D {
|
||||||
return Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Directory, tr.Model.PresetDirList().Value(i)).Layout(gtx)
|
return Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Directory, tr.Model.Preset().Dir(i)).Layout(gtx)
|
||||||
}
|
}
|
||||||
dirs := func(gtx C) D {
|
dirs := func(gtx C) D {
|
||||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(140), gtx.Constraints.Max.Y))
|
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(140), gtx.Constraints.Max.Y))
|
||||||
@@ -108,7 +108,7 @@ func (ip *InstrumentPresets) layout(gtx C) D {
|
|||||||
}
|
}
|
||||||
resultElem := func(gtx C, i int) D {
|
resultElem := func(gtx C, i int) D {
|
||||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||||
n, d, u := tr.Model.PresetResultList().Value(i)
|
n, d, u := tr.Model.Preset().SearchResult(i)
|
||||||
if u {
|
if u {
|
||||||
ln := Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Results.User, n)
|
ln := Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Results.User, n)
|
||||||
ld := Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Results.UserDir, d)
|
ld := Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Results.UserDir, d)
|
||||||
@@ -121,7 +121,7 @@ func (ip *InstrumentPresets) layout(gtx C) D {
|
|||||||
return Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Results.Builtin, n).Layout(gtx)
|
return Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Results.Builtin, n).Layout(gtx)
|
||||||
}
|
}
|
||||||
floatButtons := func(gtx C) D {
|
floatButtons := func(gtx C) D {
|
||||||
if tr.Model.DeleteUserPreset().Enabled() {
|
if tr.Model.Preset().Delete().Enabled() {
|
||||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||||
layout.Rigid(deleteUserPresetBtn.Layout),
|
layout.Rigid(deleteUserPresetBtn.Layout),
|
||||||
layout.Rigid(saveUserPresetBtn.Layout),
|
layout.Rigid(saveUserPresetBtn.Layout),
|
||||||
@@ -189,10 +189,10 @@ func (ip *InstrumentPresets) layoutSearch(gtx C) D {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
ed := func(gtx C) D {
|
ed := func(gtx C) D {
|
||||||
return ip.searchEditor.Layout(gtx, tr.Model.PresetSearchString(), tr.Theme, &tr.Theme.InstrumentEditor.UnitComment, "Search presets")
|
return ip.searchEditor.Layout(gtx, tr.Preset().SearchTerm(), tr.Theme, &tr.Theme.InstrumentEditor.UnitComment, "Search presets")
|
||||||
}
|
}
|
||||||
clr := func(gtx C) D {
|
clr := func(gtx C) D {
|
||||||
btn := ActionIconBtn(tr.ClearPresetSearch(), tr.Theme, ip.clearSearchBtn, icons.ContentClear, "Clear search")
|
btn := ActionIconBtn(tr.Preset().ClearSearch(), tr.Theme, ip.clearSearchBtn, icons.ContentClear, "Clear search")
|
||||||
return btn.Layout(gtx)
|
return btn.Layout(gtx)
|
||||||
}
|
}
|
||||||
w := func(gtx C) D {
|
w := func(gtx C) D {
|
||||||
|
|||||||
@@ -58,20 +58,20 @@ func (ip *InstrumentProperties) layout(gtx C) D {
|
|||||||
// get tracker from values
|
// get tracker from values
|
||||||
tr := TrackerFromContext(gtx)
|
tr := TrackerFromContext(gtx)
|
||||||
voiceLine := func(gtx C) D {
|
voiceLine := func(gtx C) D {
|
||||||
splitInstrumentBtn := ActionIconBtn(tr.SplitInstrument(), tr.Theme, ip.splitInstrumentBtn, icons.CommunicationCallSplit, ip.splitInstrumentHint)
|
splitInstrumentBtn := ActionIconBtn(tr.Instrument().Split(), tr.Theme, ip.splitInstrumentBtn, icons.CommunicationCallSplit, ip.splitInstrumentHint)
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
instrumentVoices := NumUpDown(tr.Model.InstrumentVoices(), tr.Theme, ip.voices, "Number of voices for this instrument")
|
instrumentVoices := NumUpDown(tr.Model.Instrument().Voices(), tr.Theme, ip.voices, "Number of voices for this instrument")
|
||||||
return instrumentVoices.Layout(gtx)
|
return instrumentVoices.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(splitInstrumentBtn.Layout),
|
layout.Rigid(splitInstrumentBtn.Layout),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
thread1btn := ToggleIconBtn(tr.Thread1(), tr.Theme, ip.threadBtns[0], icons.ImageCropSquare, icons.ImageFilter1, "Do not render instrument on thread 1", "Render instrument on thread 1")
|
thread1btn := ToggleIconBtn(tr.Instrument().Thread1(), tr.Theme, ip.threadBtns[0], icons.ImageCropSquare, icons.ImageFilter1, "Do not render instrument on thread 1", "Render instrument on thread 1")
|
||||||
thread2btn := ToggleIconBtn(tr.Thread2(), tr.Theme, ip.threadBtns[1], icons.ImageCropSquare, icons.ImageFilter2, "Do not render instrument on thread 2", "Render instrument on thread 2")
|
thread2btn := ToggleIconBtn(tr.Instrument().Thread2(), tr.Theme, ip.threadBtns[1], icons.ImageCropSquare, icons.ImageFilter2, "Do not render instrument on thread 2", "Render instrument on thread 2")
|
||||||
thread3btn := ToggleIconBtn(tr.Thread3(), tr.Theme, ip.threadBtns[2], icons.ImageCropSquare, icons.ImageFilter3, "Do not render instrument on thread 3", "Render instrument on thread 3")
|
thread3btn := ToggleIconBtn(tr.Instrument().Thread3(), tr.Theme, ip.threadBtns[2], icons.ImageCropSquare, icons.ImageFilter3, "Do not render instrument on thread 3", "Render instrument on thread 3")
|
||||||
thread4btn := ToggleIconBtn(tr.Thread4(), tr.Theme, ip.threadBtns[3], icons.ImageCropSquare, icons.ImageFilter4, "Do not render instrument on thread 4", "Render instrument on thread 4")
|
thread4btn := ToggleIconBtn(tr.Instrument().Thread4(), tr.Theme, ip.threadBtns[3], icons.ImageCropSquare, icons.ImageFilter4, "Do not render instrument on thread 4", "Render instrument on thread 4")
|
||||||
|
|
||||||
threadbtnline := func(gtx C) D {
|
threadbtnline := func(gtx C) D {
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
@@ -86,21 +86,21 @@ func (ip *InstrumentProperties) layout(gtx C) D {
|
|||||||
switch index {
|
switch index {
|
||||||
case 0:
|
case 0:
|
||||||
return layoutInstrumentPropertyLine(gtx, "Name", func(gtx C) D {
|
return layoutInstrumentPropertyLine(gtx, "Name", func(gtx C) D {
|
||||||
return ip.nameEditor.Layout(gtx, tr.InstrumentName(), tr.Theme, &tr.Theme.InstrumentEditor.InstrumentComment, "Instr")
|
return ip.nameEditor.Layout(gtx, tr.Instrument().Name(), tr.Theme, &tr.Theme.InstrumentEditor.InstrumentComment, "Instr")
|
||||||
})
|
})
|
||||||
case 2:
|
case 2:
|
||||||
return layoutInstrumentPropertyLine(gtx, "Voices", voiceLine)
|
return layoutInstrumentPropertyLine(gtx, "Voices", voiceLine)
|
||||||
case 4:
|
case 4:
|
||||||
muteBtn := ToggleIconBtn(tr.Mute(), tr.Theme, ip.muteBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.muteHint, ip.unmuteHint)
|
muteBtn := ToggleIconBtn(tr.Instrument().Mute(), tr.Theme, ip.muteBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.muteHint, ip.unmuteHint)
|
||||||
return layoutInstrumentPropertyLine(gtx, "Mute", muteBtn.Layout)
|
return layoutInstrumentPropertyLine(gtx, "Mute", muteBtn.Layout)
|
||||||
case 6:
|
case 6:
|
||||||
soloBtn := ToggleIconBtn(tr.Solo(), tr.Theme, ip.soloBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.soloHint, ip.unsoloHint)
|
soloBtn := ToggleIconBtn(tr.Instrument().Solo(), tr.Theme, ip.soloBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.soloHint, ip.unsoloHint)
|
||||||
return layoutInstrumentPropertyLine(gtx, "Solo", soloBtn.Layout)
|
return layoutInstrumentPropertyLine(gtx, "Solo", soloBtn.Layout)
|
||||||
case 8:
|
case 8:
|
||||||
return layoutInstrumentPropertyLine(gtx, "Thread", threadbtnline)
|
return layoutInstrumentPropertyLine(gtx, "Thread", threadbtnline)
|
||||||
case 10:
|
case 10:
|
||||||
return layout.UniformInset(unit.Dp(6)).Layout(gtx, func(gtx C) D {
|
return layout.UniformInset(unit.Dp(6)).Layout(gtx, func(gtx C) D {
|
||||||
return ip.commentEditor.Layout(gtx, tr.InstrumentComment(), tr.Theme, &tr.Theme.InstrumentEditor.InstrumentComment, "Comment")
|
return ip.commentEditor.Layout(gtx, tr.Instrument().Comment(), tr.Theme, &tr.Theme.InstrumentEditor.InstrumentComment, "Comment")
|
||||||
})
|
})
|
||||||
default: // odd valued list items are dividers
|
default: // odd valued list items are dividers
|
||||||
px := max(gtx.Dp(unit.Dp(1)), 1)
|
px := max(gtx.Dp(unit.Dp(1)), 1)
|
||||||
|
|||||||
@@ -102,93 +102,93 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
|||||||
switch action {
|
switch action {
|
||||||
// Actions
|
// Actions
|
||||||
case "AddTrack":
|
case "AddTrack":
|
||||||
t.AddTrack().Do()
|
t.Track().Add().Do()
|
||||||
case "DeleteTrack":
|
case "DeleteTrack":
|
||||||
t.DeleteTrack().Do()
|
t.Track().Delete().Do()
|
||||||
case "AddInstrument":
|
case "AddInstrument":
|
||||||
t.AddInstrument().Do()
|
t.Instrument().Add().Do()
|
||||||
case "DeleteInstrument":
|
case "DeleteInstrument":
|
||||||
t.DeleteInstrument().Do()
|
t.Instrument().Delete().Do()
|
||||||
case "AddUnitAfter":
|
case "AddUnitAfter":
|
||||||
t.AddUnit(false).Do()
|
t.Unit().Add(false).Do()
|
||||||
case "AddUnitBefore":
|
case "AddUnitBefore":
|
||||||
t.AddUnit(true).Do()
|
t.Unit().Add(true).Do()
|
||||||
case "DeleteUnit":
|
case "DeleteUnit":
|
||||||
t.DeleteUnit().Do()
|
t.Unit().Delete().Do()
|
||||||
case "ClearUnit":
|
case "ClearUnit":
|
||||||
t.ClearUnit().Do()
|
t.Unit().Clear().Do()
|
||||||
case "Undo":
|
case "Undo":
|
||||||
t.Undo().Do()
|
t.History().Undo().Do()
|
||||||
case "Redo":
|
case "Redo":
|
||||||
t.Redo().Do()
|
t.History().Redo().Do()
|
||||||
case "AddSemitone":
|
case "AddSemitone":
|
||||||
t.AddSemitone().Do()
|
t.Note().AddSemitone().Do()
|
||||||
case "SubtractSemitone":
|
case "SubtractSemitone":
|
||||||
t.SubtractSemitone().Do()
|
t.Note().SubtractSemitone().Do()
|
||||||
case "AddOctave":
|
case "AddOctave":
|
||||||
t.AddOctave().Do()
|
t.Note().AddOctave().Do()
|
||||||
case "SubtractOctave":
|
case "SubtractOctave":
|
||||||
t.SubtractOctave().Do()
|
t.Note().SubtractOctave().Do()
|
||||||
case "EditNoteOff":
|
case "EditNoteOff":
|
||||||
t.EditNoteOff().Do()
|
t.Note().NoteOff().Do()
|
||||||
case "RemoveUnused":
|
case "RemoveUnused":
|
||||||
t.RemoveUnused().Do()
|
t.Order().RemoveUnusedPatterns().Do()
|
||||||
case "PlayCurrentPosFollow":
|
case "PlayCurrentPosFollow":
|
||||||
t.Follow().SetValue(true)
|
t.Play().IsFollowing().SetValue(true)
|
||||||
t.PlayCurrentPos().Do()
|
t.Play().FromCurrentPos().Do()
|
||||||
case "PlayCurrentPosUnfollow":
|
case "PlayCurrentPosUnfollow":
|
||||||
t.Follow().SetValue(false)
|
t.Play().IsFollowing().SetValue(false)
|
||||||
t.PlayCurrentPos().Do()
|
t.Play().FromCurrentPos().Do()
|
||||||
case "PlaySongStartFollow":
|
case "PlaySongStartFollow":
|
||||||
t.Follow().SetValue(true)
|
t.Play().IsFollowing().SetValue(true)
|
||||||
t.PlaySongStart().Do()
|
t.Play().FromBeginning().Do()
|
||||||
case "PlaySongStartUnfollow":
|
case "PlaySongStartUnfollow":
|
||||||
t.Follow().SetValue(false)
|
t.Play().IsFollowing().SetValue(false)
|
||||||
t.PlaySongStart().Do()
|
t.Play().FromBeginning().Do()
|
||||||
case "PlaySelectedFollow":
|
case "PlaySelectedFollow":
|
||||||
t.Follow().SetValue(true)
|
t.Play().IsFollowing().SetValue(true)
|
||||||
t.PlaySelected().Do()
|
t.Play().FromSelected().Do()
|
||||||
case "PlaySelectedUnfollow":
|
case "PlaySelectedUnfollow":
|
||||||
t.Follow().SetValue(false)
|
t.Play().IsFollowing().SetValue(false)
|
||||||
t.PlaySelected().Do()
|
t.Play().FromSelected().Do()
|
||||||
case "PlayLoopFollow":
|
case "PlayLoopFollow":
|
||||||
t.Follow().SetValue(true)
|
t.Play().IsFollowing().SetValue(true)
|
||||||
t.PlayFromLoopStart().Do()
|
t.Play().FromLoopBeginning().Do()
|
||||||
case "PlayLoopUnfollow":
|
case "PlayLoopUnfollow":
|
||||||
t.Follow().SetValue(false)
|
t.Play().IsFollowing().SetValue(false)
|
||||||
t.PlayFromLoopStart().Do()
|
t.Play().FromLoopBeginning().Do()
|
||||||
case "StopPlaying":
|
case "StopPlaying":
|
||||||
t.StopPlaying().Do()
|
t.Play().Stop().Do()
|
||||||
case "AddOrderRowBefore":
|
case "AddOrderRowBefore":
|
||||||
t.AddOrderRow(true).Do()
|
t.Order().AddRow(true).Do()
|
||||||
case "AddOrderRowAfter":
|
case "AddOrderRowAfter":
|
||||||
t.AddOrderRow(false).Do()
|
t.Order().AddRow(false).Do()
|
||||||
case "DeleteOrderRowBackwards":
|
case "DeleteOrderRowBackwards":
|
||||||
t.DeleteOrderRow(true).Do()
|
t.Order().DeleteRow(true).Do()
|
||||||
case "DeleteOrderRowForwards":
|
case "DeleteOrderRowForwards":
|
||||||
t.DeleteOrderRow(false).Do()
|
t.Order().DeleteRow(false).Do()
|
||||||
case "NewSong":
|
case "NewSong":
|
||||||
t.NewSong().Do()
|
t.Song().New().Do()
|
||||||
case "OpenSong":
|
case "OpenSong":
|
||||||
t.OpenSong().Do()
|
t.Song().Open().Do()
|
||||||
case "Quit":
|
case "Quit":
|
||||||
if canQuit {
|
if canQuit {
|
||||||
t.RequestQuit().Do()
|
t.RequestQuit().Do()
|
||||||
}
|
}
|
||||||
case "SaveSong":
|
case "SaveSong":
|
||||||
t.SaveSong().Do()
|
t.Song().Save().Do()
|
||||||
case "SaveSongAs":
|
case "SaveSongAs":
|
||||||
t.SaveSongAs().Do()
|
t.Song().SaveAs().Do()
|
||||||
case "ExportWav":
|
case "ExportWav":
|
||||||
t.Export().Do()
|
t.Song().Export().Do()
|
||||||
case "ExportFloat":
|
case "ExportFloat":
|
||||||
t.ExportFloat().Do()
|
t.Song().ExportFloat().Do()
|
||||||
case "ExportInt16":
|
case "ExportInt16":
|
||||||
t.ExportInt16().Do()
|
t.Song().ExportInt16().Do()
|
||||||
case "SplitTrack":
|
case "SplitTrack":
|
||||||
t.SplitTrack().Do()
|
t.Track().Split().Do()
|
||||||
case "SplitInstrument":
|
case "SplitInstrument":
|
||||||
t.SplitInstrument().Do()
|
t.Instrument().Split().Do()
|
||||||
case "ShowManual":
|
case "ShowManual":
|
||||||
t.ShowManual().Do()
|
t.ShowManual().Do()
|
||||||
case "AskHelp":
|
case "AskHelp":
|
||||||
@@ -199,72 +199,72 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
|||||||
t.ShowLicense().Do()
|
t.ShowLicense().Do()
|
||||||
// Booleans
|
// Booleans
|
||||||
case "PanicToggle":
|
case "PanicToggle":
|
||||||
t.Panic().Toggle()
|
t.Play().Panicked().Toggle()
|
||||||
case "RecordingToggle":
|
case "RecordingToggle":
|
||||||
t.IsRecording().Toggle()
|
t.Play().IsRecording().Toggle()
|
||||||
case "PlayingToggleFollow":
|
case "PlayingToggleFollow":
|
||||||
t.Follow().SetValue(true)
|
t.Play().IsFollowing().SetValue(true)
|
||||||
t.Playing().Toggle()
|
t.Play().Started().Toggle()
|
||||||
case "PlayingToggleUnfollow":
|
case "PlayingToggleUnfollow":
|
||||||
t.Follow().SetValue(false)
|
t.Play().IsFollowing().SetValue(false)
|
||||||
t.Playing().Toggle()
|
t.Play().Started().Toggle()
|
||||||
case "InstrEnlargedToggle":
|
case "InstrEnlargedToggle":
|
||||||
t.InstrEnlarged().Toggle()
|
t.Play().TrackerHidden().Toggle()
|
||||||
case "LinkInstrTrackToggle":
|
case "LinkInstrTrackToggle":
|
||||||
t.LinkInstrTrack().Toggle()
|
t.Track().LinkInstrument().Toggle()
|
||||||
case "FollowToggle":
|
case "FollowToggle":
|
||||||
t.Follow().Toggle()
|
t.Play().IsFollowing().Toggle()
|
||||||
case "UnitDisabledToggle":
|
case "UnitDisabledToggle":
|
||||||
t.UnitDisabled().Toggle()
|
t.Unit().Disabled().Toggle()
|
||||||
case "LoopToggle":
|
case "LoopToggle":
|
||||||
t.LoopToggle().Toggle()
|
t.Play().IsLooping().Toggle()
|
||||||
case "UniquePatternsToggle":
|
case "UniquePatternsToggle":
|
||||||
t.UniquePatterns().Toggle()
|
t.Note().UniquePatterns().Toggle()
|
||||||
case "MuteToggle":
|
case "MuteToggle":
|
||||||
t.Mute().Toggle()
|
t.Instrument().Mute().Toggle()
|
||||||
case "SoloToggle":
|
case "SoloToggle":
|
||||||
t.Solo().Toggle()
|
t.Instrument().Solo().Toggle()
|
||||||
// Integers
|
// Integers
|
||||||
case "InstrumentVoicesAdd":
|
case "InstrumentVoicesAdd":
|
||||||
t.Model.InstrumentVoices().Add(1)
|
t.Instrument().Voices().Add(1)
|
||||||
case "InstrumentVoicesSubtract":
|
case "InstrumentVoicesSubtract":
|
||||||
t.Model.InstrumentVoices().Add(-1)
|
t.Instrument().Voices().Add(-1)
|
||||||
case "TrackVoicesAdd":
|
case "TrackVoicesAdd":
|
||||||
t.TrackVoices().Add(1)
|
t.Track().Voices().Add(1)
|
||||||
case "TrackVoicesSubtract":
|
case "TrackVoicesSubtract":
|
||||||
t.TrackVoices().Add(-1)
|
t.Track().Voices().Add(-1)
|
||||||
case "SongLengthAdd":
|
case "SongLengthAdd":
|
||||||
t.SongLength().Add(1)
|
t.Song().Length().Add(1)
|
||||||
case "SongLengthSubtract":
|
case "SongLengthSubtract":
|
||||||
t.SongLength().Add(-1)
|
t.Song().Length().Add(-1)
|
||||||
case "BPMAdd":
|
case "BPMAdd":
|
||||||
t.BPM().Add(1)
|
t.Song().BPM().Add(1)
|
||||||
case "BPMSubtract":
|
case "BPMSubtract":
|
||||||
t.BPM().Add(-1)
|
t.Song().BPM().Add(-1)
|
||||||
case "RowsPerPatternAdd":
|
case "RowsPerPatternAdd":
|
||||||
t.RowsPerPattern().Add(1)
|
t.Song().RowsPerPattern().Add(1)
|
||||||
case "RowsPerPatternSubtract":
|
case "RowsPerPatternSubtract":
|
||||||
t.RowsPerPattern().Add(-1)
|
t.Song().RowsPerPattern().Add(-1)
|
||||||
case "RowsPerBeatAdd":
|
case "RowsPerBeatAdd":
|
||||||
t.RowsPerBeat().Add(1)
|
t.Song().RowsPerBeat().Add(1)
|
||||||
case "RowsPerBeatSubtract":
|
case "RowsPerBeatSubtract":
|
||||||
t.RowsPerBeat().Add(-1)
|
t.Song().RowsPerBeat().Add(-1)
|
||||||
case "StepAdd":
|
case "StepAdd":
|
||||||
t.Step().Add(1)
|
t.Note().Step().Add(1)
|
||||||
case "StepSubtract":
|
case "StepSubtract":
|
||||||
t.Step().Add(-1)
|
t.Note().Step().Add(-1)
|
||||||
case "OctaveAdd":
|
case "OctaveAdd":
|
||||||
t.Octave().Add(1)
|
t.Note().Octave().Add(1)
|
||||||
case "OctaveSubtract":
|
case "OctaveSubtract":
|
||||||
t.Octave().Add(-1)
|
t.Note().Octave().Add(-1)
|
||||||
// Other miscellaneous
|
// Other miscellaneous
|
||||||
case "Paste":
|
case "Paste":
|
||||||
gtx.Execute(clipboard.ReadCmd{Tag: t})
|
gtx.Execute(clipboard.ReadCmd{Tag: t})
|
||||||
case "OrderEditorFocus":
|
case "OrderEditorFocus":
|
||||||
t.InstrEnlarged().SetValue(false)
|
t.Play().TrackerHidden().SetValue(false)
|
||||||
gtx.Execute(key.FocusCmd{Tag: t.OrderEditor.scrollTable})
|
gtx.Execute(key.FocusCmd{Tag: t.OrderEditor.scrollTable})
|
||||||
case "TrackEditorFocus":
|
case "TrackEditorFocus":
|
||||||
t.InstrEnlarged().SetValue(false)
|
t.Play().TrackerHidden().SetValue(false)
|
||||||
gtx.Execute(key.FocusCmd{Tag: t.TrackEditor.scrollTable})
|
gtx.Execute(key.FocusCmd{Tag: t.TrackEditor.scrollTable})
|
||||||
case "InstrumentListFocus":
|
case "InstrumentListFocus":
|
||||||
gtx.Execute(key.FocusCmd{Tag: t.PatchPanel.instrList.instrumentDragList})
|
gtx.Execute(key.FocusCmd{Tag: t.PatchPanel.instrList.instrumentDragList})
|
||||||
@@ -289,8 +289,8 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
instr := t.Model.Instruments().Selected()
|
instr := t.Model.Instrument().List().Selected()
|
||||||
n := noteAsValue(t.Model.Octave().Value(), val-12)
|
n := noteAsValue(t.Model.Note().Octave().Value(), val-12)
|
||||||
t.KeyNoteMap.Press(e.Name, tracker.NoteEvent{Channel: instr, Note: n})
|
t.KeyNoteMap.Press(e.Name, tracker.NoteEvent{Channel: instr, Note: n})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,9 +92,9 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
|||||||
UniqueBtn: new(Clickable),
|
UniqueBtn: new(Clickable),
|
||||||
TrackMidiInBtn: new(Clickable),
|
TrackMidiInBtn: new(Clickable),
|
||||||
scrollTable: NewScrollTable(
|
scrollTable: NewScrollTable(
|
||||||
model.Notes().Table(),
|
model.Note().Table(),
|
||||||
model.Tracks(),
|
model.Track().List(),
|
||||||
model.NoteRows(),
|
model.Note().RowList(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
for k, a := range keyBindingMap {
|
for k, a := range keyBindingMap {
|
||||||
@@ -137,10 +137,10 @@ func (te *NoteEditor) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
for gtx.Focused(te.scrollTable) && len(t.noteEvents) > 0 {
|
for gtx.Focused(te.scrollTable) && len(t.noteEvents) > 0 {
|
||||||
ev := t.noteEvents[0]
|
ev := t.noteEvents[0]
|
||||||
ev.IsTrack = true
|
ev.IsTrack = true
|
||||||
ev.Channel = t.Model.Notes().Cursor().X
|
ev.Channel = t.Model.Note().Cursor().X
|
||||||
ev.Source = te
|
ev.Source = te
|
||||||
if ev.On {
|
if ev.On {
|
||||||
t.Model.Notes().Input(ev.Note)
|
t.Model.Note().Input(ev.Note)
|
||||||
}
|
}
|
||||||
copy(t.noteEvents, t.noteEvents[1:])
|
copy(t.noteEvents, t.noteEvents[1:])
|
||||||
t.noteEvents = t.noteEvents[:len(t.noteEvents)-1]
|
t.noteEvents = t.noteEvents[:len(t.noteEvents)-1]
|
||||||
@@ -163,22 +163,22 @@ func (te *NoteEditor) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
|
|
||||||
func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
|
func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
|
||||||
return Surface{Height: 4, Focus: te.scrollTable.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D {
|
return Surface{Height: 4, Focus: te.scrollTable.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D {
|
||||||
addSemitoneBtn := ActionBtn(t.AddSemitone(), t.Theme, te.AddSemitoneBtn, "+1", "Add semitone")
|
addSemitoneBtn := ActionBtn(t.Note().AddSemitone(), t.Theme, te.AddSemitoneBtn, "+1", "Add semitone")
|
||||||
subtractSemitoneBtn := ActionBtn(t.SubtractSemitone(), t.Theme, te.SubtractSemitoneBtn, "-1", "Subtract semitone")
|
subtractSemitoneBtn := ActionBtn(t.Note().SubtractSemitone(), t.Theme, te.SubtractSemitoneBtn, "-1", "Subtract semitone")
|
||||||
addOctaveBtn := ActionBtn(t.AddOctave(), t.Theme, te.AddOctaveBtn, "+12", "Add octave")
|
addOctaveBtn := ActionBtn(t.Note().AddOctave(), t.Theme, te.AddOctaveBtn, "+12", "Add octave")
|
||||||
subtractOctaveBtn := ActionBtn(t.SubtractOctave(), t.Theme, te.SubtractOctaveBtn, "-12", "Subtract octave")
|
subtractOctaveBtn := ActionBtn(t.Note().SubtractOctave(), t.Theme, te.SubtractOctaveBtn, "-12", "Subtract octave")
|
||||||
noteOffBtn := ActionBtn(t.EditNoteOff(), t.Theme, te.NoteOffBtn, "Note Off", "")
|
noteOffBtn := ActionBtn(t.Note().NoteOff(), t.Theme, te.NoteOffBtn, "Note Off", "")
|
||||||
deleteTrackBtn := ActionIconBtn(t.DeleteTrack(), t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint)
|
deleteTrackBtn := ActionIconBtn(t.Track().Delete(), t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint)
|
||||||
splitTrackBtn := ActionIconBtn(t.SplitTrack(), t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint)
|
splitTrackBtn := ActionIconBtn(t.Track().Split(), t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint)
|
||||||
newTrackBtn := ActionIconBtn(t.AddTrack(), t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
|
newTrackBtn := ActionIconBtn(t.Track().Add(), t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
|
||||||
trackVoices := NumUpDown(t.Model.TrackVoices(), t.Theme, te.TrackVoices, "Track voices")
|
trackVoices := NumUpDown(t.Model.Track().Voices(), t.Theme, te.TrackVoices, "Track voices")
|
||||||
in := layout.UniformInset(unit.Dp(1))
|
in := layout.UniformInset(unit.Dp(1))
|
||||||
trackVoicesInsetted := func(gtx C) D {
|
trackVoicesInsetted := func(gtx C) D {
|
||||||
return in.Layout(gtx, trackVoices.Layout)
|
return in.Layout(gtx, trackVoices.Layout)
|
||||||
}
|
}
|
||||||
effectBtn := ToggleBtn(t.Effect(), t.Theme, te.EffectBtn, "Hex", "Input notes as hex values")
|
effectBtn := ToggleBtn(t.Track().Effect(), t.Theme, te.EffectBtn, "Hex", "Input notes as hex values")
|
||||||
uniqueBtn := ToggleIconBtn(t.UniquePatterns(), t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip)
|
uniqueBtn := ToggleIconBtn(t.Note().UniquePatterns(), t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip)
|
||||||
midiInBtn := ToggleBtn(t.TrackMidiIn(), t.Theme, te.TrackMidiInBtn, "MIDI", "Input notes from MIDI keyboard")
|
midiInBtn := ToggleBtn(t.MIDI().InputtingNotes(), t.Theme, te.TrackMidiInBtn, "MIDI", "Input notes from MIDI keyboard")
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
|
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
|
||||||
layout.Rigid(addSemitoneBtn.Layout),
|
layout.Rigid(addSemitoneBtn.Layout),
|
||||||
@@ -220,13 +220,13 @@ var notes = []string{
|
|||||||
func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||||
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
||||||
|
|
||||||
beatMarkerDensity := t.RowsPerBeat().Value()
|
beatMarkerDensity := t.Song().RowsPerBeat().Value()
|
||||||
switch beatMarkerDensity {
|
switch beatMarkerDensity {
|
||||||
case 0, 1, 2:
|
case 0, 1, 2:
|
||||||
beatMarkerDensity = 4
|
beatMarkerDensity = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
playSongRow := t.PlaySongRow()
|
playSongRow := t.Play().SongRow()
|
||||||
pxWidth := gtx.Dp(trackColWidth)
|
pxWidth := gtx.Dp(trackColWidth)
|
||||||
pxHeight := gtx.Dp(trackRowHeight)
|
pxHeight := gtx.Dp(trackRowHeight)
|
||||||
pxPatMarkWidth := gtx.Dp(trackPatMarkWidth)
|
pxPatMarkWidth := gtx.Dp(trackPatMarkWidth)
|
||||||
@@ -235,7 +235,7 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
|||||||
colTitle := func(gtx C, i int) D {
|
colTitle := func(gtx C, i int) D {
|
||||||
h := gtx.Dp(trackColTitleHeight)
|
h := gtx.Dp(trackColTitleHeight)
|
||||||
gtx.Constraints = layout.Exact(image.Pt(pxWidth, h))
|
gtx.Constraints = layout.Exact(image.Pt(pxWidth, h))
|
||||||
Label(t.Theme, &t.Theme.NoteEditor.TrackTitle, t.Model.TrackTitle(i)).Layout(gtx)
|
Label(t.Theme, &t.Theme.NoteEditor.TrackTitle, t.Model.Track().Item(i).Title).Layout(gtx)
|
||||||
return D{Size: image.Pt(pxWidth, h)}
|
return D{Size: image.Pt(pxWidth, h)}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
|||||||
} else if mod(j, beatMarkerDensity) == 0 {
|
} else if mod(j, beatMarkerDensity) == 0 {
|
||||||
paint.FillShape(gtx.Ops, t.Theme.NoteEditor.OneBeat, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
paint.FillShape(gtx.Ops, t.Theme.NoteEditor.OneBeat, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
||||||
}
|
}
|
||||||
if t.Model.Playing().Value() && j == playSongRow {
|
if t.Model.Play().Started().Value() && j == playSongRow {
|
||||||
paint.FillShape(gtx.Ops, t.Theme.NoteEditor.Play, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
paint.FillShape(gtx.Ops, t.Theme.NoteEditor.Play, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
||||||
}
|
}
|
||||||
return D{}
|
return D{}
|
||||||
@@ -256,14 +256,14 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
|||||||
patternRowOp := colorOp(gtx, t.Theme.NoteEditor.PatternRow.Color)
|
patternRowOp := colorOp(gtx, t.Theme.NoteEditor.PatternRow.Color)
|
||||||
|
|
||||||
rowTitle := func(gtx C, j int) D {
|
rowTitle := func(gtx C, j int) D {
|
||||||
rpp := max(t.RowsPerPattern().Value(), 1)
|
rpp := max(t.Song().RowsPerPattern().Value(), 1)
|
||||||
pat := j / rpp
|
pat := j / rpp
|
||||||
row := j % rpp
|
row := j % rpp
|
||||||
w := pxPatMarkWidth + pxRowMarkWidth
|
w := pxPatMarkWidth + pxRowMarkWidth
|
||||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||||
if row == 0 {
|
if row == 0 {
|
||||||
op := orderRowOp
|
op := orderRowOp
|
||||||
if l := t.Loop(); pat >= l.Start && pat < l.Start+l.Length {
|
if l := t.Play().Loop(); pat >= l.Start && pat < l.Start+l.Length {
|
||||||
op = loopColorOp
|
op = loopColorOp
|
||||||
}
|
}
|
||||||
widget.Label{}.Layout(gtx, t.Theme.Material.Shaper, t.Theme.NoteEditor.OrderRow.Font, t.Theme.NoteEditor.OrderRow.TextSize, hexStr[pat&255], op)
|
widget.Label{}.Layout(gtx, t.Theme.Material.Shaper, t.Theme.NoteEditor.OrderRow.Font, t.Theme.NoteEditor.OrderRow.TextSize, hexStr[pat&255], op)
|
||||||
@@ -276,7 +276,7 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
|||||||
cursor := te.scrollTable.Table.Cursor()
|
cursor := te.scrollTable.Table.Cursor()
|
||||||
drawSelection := cursor != te.scrollTable.Table.Cursor2()
|
drawSelection := cursor != te.scrollTable.Table.Cursor2()
|
||||||
selection := te.scrollTable.Table.Range()
|
selection := te.scrollTable.Table.Range()
|
||||||
hasTrackMidiIn := t.Model.TrackMidiIn().Value()
|
hasTrackMidiIn := t.MIDI().InputtingNotes().Value()
|
||||||
|
|
||||||
patternNoOp := colorOp(gtx, t.Theme.NoteEditor.PatternNo.Color)
|
patternNoOp := colorOp(gtx, t.Theme.NoteEditor.PatternNo.Color)
|
||||||
uniqueOp := colorOp(gtx, t.Theme.NoteEditor.Unique.Color)
|
uniqueOp := colorOp(gtx, t.Theme.NoteEditor.Unique.Color)
|
||||||
@@ -305,7 +305,7 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// draw the pattern marker
|
// draw the pattern marker
|
||||||
rpp := max(t.RowsPerPattern().Value(), 1)
|
rpp := max(t.Song().RowsPerPattern().Value(), 1)
|
||||||
pat := y / rpp
|
pat := y / rpp
|
||||||
row := y % rpp
|
row := y % rpp
|
||||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||||
@@ -313,13 +313,13 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
|||||||
if row == 0 { // draw the pattern marker
|
if row == 0 { // draw the pattern marker
|
||||||
widget.Label{}.Layout(gtx, t.Theme.Material.Shaper, t.Theme.NoteEditor.PatternNo.Font, t.Theme.NoteEditor.PatternNo.TextSize, patternIndexToString(s), patternNoOp)
|
widget.Label{}.Layout(gtx, t.Theme.Material.Shaper, t.Theme.NoteEditor.PatternNo.Font, t.Theme.NoteEditor.PatternNo.TextSize, patternIndexToString(s), patternNoOp)
|
||||||
}
|
}
|
||||||
if row == 1 && t.Model.PatternUnique(x, s) { // draw a * if the pattern is unique
|
if row == 1 && t.Order().PatternUnique(x, s) { // draw a * if the pattern is unique
|
||||||
widget.Label{}.Layout(gtx, t.Theme.Material.Shaper, t.Theme.NoteEditor.Unique.Font, t.Theme.NoteEditor.Unique.TextSize, "*", uniqueOp)
|
widget.Label{}.Layout(gtx, t.Theme.Material.Shaper, t.Theme.NoteEditor.Unique.Font, t.Theme.NoteEditor.Unique.TextSize, "*", uniqueOp)
|
||||||
}
|
}
|
||||||
op := noteOp
|
op := noteOp
|
||||||
val := noteName[byte(t.Model.Notes().Value(tracker.Point{X: x, Y: y}))]
|
val := noteName[byte(t.Model.Note().At(tracker.Point{X: x, Y: y}))]
|
||||||
if t.Model.Notes().Effect(x) {
|
if t.Model.Track().Item(x).Effect {
|
||||||
val = noteHex[byte(t.Model.Notes().Value(tracker.Point{X: x, Y: y}))]
|
val = noteHex[byte(t.Model.Note().At(tracker.Point{X: x, Y: y}))]
|
||||||
}
|
}
|
||||||
widget.Label{Alignment: text.Middle}.Layout(gtx, t.Theme.Material.Shaper, t.Theme.NoteEditor.Note.Font, t.Theme.NoteEditor.Note.TextSize, val, op)
|
widget.Label{Alignment: text.Middle}.Layout(gtx, t.Theme.Material.Shaper, t.Theme.NoteEditor.Note.Font, t.Theme.NoteEditor.Note.TextSize, val, op)
|
||||||
return D{Size: image.Pt(pxWidth, pxHeight)}
|
return D{Size: image.Pt(pxWidth, pxHeight)}
|
||||||
@@ -347,9 +347,9 @@ func colorOp(gtx C, c color.NRGBA) op.CallOp {
|
|||||||
func (te *NoteEditor) paintColumnCell(gtx C, x int, t *Tracker, c color.NRGBA) {
|
func (te *NoteEditor) paintColumnCell(gtx C, x int, t *Tracker, c color.NRGBA) {
|
||||||
cw := gtx.Constraints.Min.X
|
cw := gtx.Constraints.Min.X
|
||||||
cx := 0
|
cx := 0
|
||||||
if t.Model.Notes().Effect(x) {
|
if t.Model.Track().Item(x).Effect {
|
||||||
cw /= 2
|
cw /= 2
|
||||||
if t.Model.Notes().LowNibble() {
|
if t.Model.Note().LowNibble() {
|
||||||
cx += cw
|
cx += cw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,9 +373,9 @@ func noteAsValue(octave, note int) byte {
|
|||||||
|
|
||||||
func (te *NoteEditor) command(t *Tracker, e key.Event) {
|
func (te *NoteEditor) command(t *Tracker, e key.Event) {
|
||||||
var n byte
|
var n byte
|
||||||
if t.Model.Notes().Effect(te.scrollTable.Table.Cursor().X) {
|
if t.Model.Track().Item(te.scrollTable.Table.Cursor().X).Effect {
|
||||||
if nibbleValue, err := strconv.ParseInt(string(e.Name), 16, 8); err == nil {
|
if nibbleValue, err := strconv.ParseInt(string(e.Name), 16, 8); err == nil {
|
||||||
ev := t.Model.Notes().InputNibble(byte(nibbleValue))
|
ev := t.Model.Note().InputNibble(byte(nibbleValue))
|
||||||
t.KeyNoteMap.Press(e.Name, ev)
|
t.KeyNoteMap.Press(e.Name, ev)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -384,7 +384,7 @@ func (te *NoteEditor) command(t *Tracker, e key.Event) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if action == "NoteOff" {
|
if action == "NoteOff" {
|
||||||
ev := t.Model.Notes().Input(0)
|
ev := t.Model.Note().Input(0)
|
||||||
t.KeyNoteMap.Press(e.Name, ev)
|
t.KeyNoteMap.Press(e.Name, ev)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -393,8 +393,8 @@ func (te *NoteEditor) command(t *Tracker, e key.Event) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
n = noteAsValue(t.Octave().Value(), val-12)
|
n = noteAsValue(t.Note().Octave().Value(), val-12)
|
||||||
ev := t.Model.Notes().Input(n)
|
ev := t.Model.Note().Input(n)
|
||||||
t.KeyNoteMap.Press(e.Name, ev)
|
t.KeyNoteMap.Press(e.Name, ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ func NewOrderEditor(m *tracker.Model) *OrderEditor {
|
|||||||
return &OrderEditor{
|
return &OrderEditor{
|
||||||
scrollTable: NewScrollTable(
|
scrollTable: NewScrollTable(
|
||||||
m.Order().Table(),
|
m.Order().Table(),
|
||||||
m.Tracks(),
|
m.Track().List(),
|
||||||
m.OrderRows(),
|
m.Order().RowList(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,12 +67,12 @@ func (oe *OrderEditor) Layout(gtx C) D {
|
|||||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||||
defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(0, 0), -90*math.Pi/180).Offset(f32.Point{X: 0, Y: float32(h)})).Push(gtx.Ops).Pop()
|
defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(0, 0), -90*math.Pi/180).Offset(f32.Point{X: 0, Y: float32(h)})).Push(gtx.Ops).Pop()
|
||||||
gtx.Constraints = layout.Exact(image.Pt(1e6, 1e6))
|
gtx.Constraints = layout.Exact(image.Pt(1e6, 1e6))
|
||||||
Label(t.Theme, &t.Theme.OrderEditor.TrackTitle, t.Model.TrackTitle(i)).Layout(gtx)
|
Label(t.Theme, &t.Theme.OrderEditor.TrackTitle, t.Model.Track().Item(i).Title).Layout(gtx)
|
||||||
return D{Size: image.Pt(gtx.Dp(patternCellWidth), h)}
|
return D{Size: image.Pt(gtx.Dp(patternCellWidth), h)}
|
||||||
}
|
}
|
||||||
|
|
||||||
rowTitleBg := func(gtx C, j int) D {
|
rowTitleBg := func(gtx C, j int) D {
|
||||||
if t.Model.Playing().Value() && j == t.PlayPosition().OrderRow {
|
if t.Model.Play().Started().Value() && j == t.Play().Position().OrderRow {
|
||||||
paint.FillShape(gtx.Ops, t.Theme.OrderEditor.Play, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Dp(patternCellHeight))}.Op())
|
paint.FillShape(gtx.Ops, t.Theme.OrderEditor.Play, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, gtx.Dp(patternCellHeight))}.Op())
|
||||||
}
|
}
|
||||||
return D{}
|
return D{}
|
||||||
@@ -84,7 +84,7 @@ func (oe *OrderEditor) Layout(gtx C) D {
|
|||||||
rowTitle := func(gtx C, j int) D {
|
rowTitle := func(gtx C, j int) D {
|
||||||
w := gtx.Dp(unit.Dp(30))
|
w := gtx.Dp(unit.Dp(30))
|
||||||
callOp := rowMarkerPatternTextColorOp
|
callOp := rowMarkerPatternTextColorOp
|
||||||
if l := t.Loop(); j >= l.Start && j < l.Start+l.Length {
|
if l := t.Play().Loop(); j >= l.Start && j < l.Start+l.Length {
|
||||||
callOp = loopMarkerColorOp
|
callOp = loopMarkerColorOp
|
||||||
}
|
}
|
||||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||||
@@ -184,14 +184,14 @@ func (oe *OrderEditor) command(t *Tracker, e key.Event) {
|
|||||||
switch e.Name {
|
switch e.Name {
|
||||||
case key.NameDeleteBackward:
|
case key.NameDeleteBackward:
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
if e.Modifiers.Contain(key.ModShortcut) {
|
||||||
t.Model.DeleteOrderRow(true).Do()
|
t.Model.Order().DeleteRow(true).Do()
|
||||||
}
|
}
|
||||||
case key.NameDeleteForward:
|
case key.NameDeleteForward:
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
if e.Modifiers.Contain(key.ModShortcut) {
|
||||||
t.Model.DeleteOrderRow(false).Do()
|
t.Model.Order().DeleteRow(false).Do()
|
||||||
}
|
}
|
||||||
case key.NameReturn:
|
case key.NameReturn:
|
||||||
t.Model.AddOrderRow(e.Modifiers.Contain(key.ModShortcut)).Do()
|
t.Model.Order().AddRow(e.Modifiers.Contain(key.ModShortcut)).Do()
|
||||||
}
|
}
|
||||||
if iv, err := strconv.Atoi(string(e.Name)); err == nil {
|
if iv, err := strconv.Atoi(string(e.Name)); err == nil {
|
||||||
t.Model.Order().SetValue(oe.scrollTable.Table.Cursor(), iv)
|
t.Model.Order().SetValue(oe.scrollTable.Table.Cursor(), iv)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"github.com/vsariola/sointu/tracker"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -20,12 +19,11 @@ type (
|
|||||||
|
|
||||||
Oscilloscope struct {
|
Oscilloscope struct {
|
||||||
Theme *Theme
|
Theme *Theme
|
||||||
Model *tracker.ScopeModel
|
|
||||||
State *OscilloscopeState
|
State *OscilloscopeState
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewOscilloscope(model *tracker.Model) *OscilloscopeState {
|
func NewOscilloscope() *OscilloscopeState {
|
||||||
return &OscilloscopeState{
|
return &OscilloscopeState{
|
||||||
plot: NewPlot(plotRange{0, 1}, plotRange{-1, 1}, 0),
|
plot: NewPlot(plotRange{0, 1}, plotRange{-1, 1}, 0),
|
||||||
onceBtn: new(Clickable),
|
onceBtn: new(Clickable),
|
||||||
@@ -35,10 +33,9 @@ func NewOscilloscope(model *tracker.Model) *OscilloscopeState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Scope(th *Theme, m *tracker.ScopeModel, st *OscilloscopeState) Oscilloscope {
|
func Scope(th *Theme, st *OscilloscopeState) Oscilloscope {
|
||||||
return Oscilloscope{
|
return Oscilloscope{
|
||||||
Theme: th,
|
Theme: th,
|
||||||
Model: m,
|
|
||||||
State: st,
|
State: st,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,15 +45,15 @@ func (s *Oscilloscope) Layout(gtx C) D {
|
|||||||
leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout
|
leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout
|
||||||
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout
|
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout
|
||||||
|
|
||||||
triggerChannel := NumUpDown(s.Model.TriggerChannel(), s.Theme, s.State.triggerChannelNumber, "Trigger channel")
|
triggerChannel := NumUpDown(t.Scope().TriggerChannel(), s.Theme, s.State.triggerChannelNumber, "Trigger channel")
|
||||||
lengthInBeats := NumUpDown(s.Model.LengthInBeats(), s.Theme, s.State.lengthInBeatsNumber, "Buffer length in beats")
|
lengthInBeats := NumUpDown(t.Scope().LengthInBeats(), s.Theme, s.State.lengthInBeatsNumber, "Buffer length in beats")
|
||||||
|
|
||||||
onceBtn := ToggleBtn(s.Model.Once(), s.Theme, s.State.onceBtn, "Once", "Trigger once on next event")
|
onceBtn := ToggleBtn(t.Scope().Once(), s.Theme, s.State.onceBtn, "Once", "Trigger once on next event")
|
||||||
wrapBtn := ToggleBtn(s.Model.Wrap(), s.Theme, s.State.wrapBtn, "Wrap", "Wrap buffer when full")
|
wrapBtn := ToggleBtn(t.Scope().Wrap(), s.Theme, s.State.wrapBtn, "Wrap", "Wrap buffer when full")
|
||||||
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Flexed(1, func(gtx C) D {
|
layout.Flexed(1, func(gtx C) D {
|
||||||
w := s.Model.Waveform()
|
w := t.Scope().Waveform()
|
||||||
cx := float32(w.Cursor) / float32(len(w.Buffer))
|
cx := float32(w.Cursor) / float32(len(w.Buffer))
|
||||||
|
|
||||||
data := func(chn int, xr plotRange) (yr plotRange, ok bool) {
|
data := func(chn int, xr plotRange) (yr plotRange, ok bool) {
|
||||||
@@ -65,9 +62,10 @@ func (s *Oscilloscope) Layout(gtx C) D {
|
|||||||
if x1 > x2 {
|
if x1 > x2 {
|
||||||
return plotRange{}, false
|
return plotRange{}, false
|
||||||
}
|
}
|
||||||
|
step := max((x2-x1)/1000, 1) // if the range is too large, sample only ~ 1000 points
|
||||||
y1 := float32(math.Inf(-1))
|
y1 := float32(math.Inf(-1))
|
||||||
y2 := float32(math.Inf(+1))
|
y2 := float32(math.Inf(+1))
|
||||||
for i := x1; i <= x2; i++ {
|
for i := x1; i <= x2; i += step {
|
||||||
sample := w.Buffer[i][chn]
|
sample := w.Buffer[i][chn]
|
||||||
y1 = max(y1, sample)
|
y1 = max(y1, sample)
|
||||||
y2 = min(y2, sample)
|
y2 = min(y2, sample)
|
||||||
@@ -75,9 +73,9 @@ func (s *Oscilloscope) Layout(gtx C) D {
|
|||||||
return plotRange{-y1, -y2}, true
|
return plotRange{-y1, -y2}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
rpb := max(t.Model.RowsPerBeat().Value(), 1)
|
rpb := max(t.Song().RowsPerBeat().Value(), 1)
|
||||||
xticks := func(r plotRange, count int, yield func(pos float32, label string)) {
|
xticks := func(r plotRange, count int, yield func(pos float32, label string)) {
|
||||||
l := s.Model.LengthInBeats().Value() * rpb
|
l := t.Scope().LengthInBeats().Value() * rpb
|
||||||
a := max(int(math.Ceil(float64(r.a*float32(l)))), 0)
|
a := max(int(math.Ceil(float64(r.a*float32(l)))), 0)
|
||||||
b := min(int(math.Floor(float64(r.b*float32(l)))), l)
|
b := min(int(math.Floor(float64(r.b*float32(l)))), l)
|
||||||
step := 1
|
step := 1
|
||||||
|
|||||||
@@ -128,9 +128,9 @@ func (p ParamWidget) Layout(gtx C) D {
|
|||||||
title := Label(p.Theme, &p.Theme.UnitEditor.Name, p.Parameter.Name())
|
title := Label(p.Theme, &p.Theme.UnitEditor.Name, p.Parameter.Name())
|
||||||
t := TrackerFromContext(gtx)
|
t := TrackerFromContext(gtx)
|
||||||
widget := func(gtx C) D {
|
widget := func(gtx C) D {
|
||||||
if port, ok := p.Parameter.Port(); t.IsChoosingSendTarget() && ok {
|
if port, ok := p.Parameter.Port(); t.Params().IsChoosingSendTarget() && ok {
|
||||||
for p.State.clickable.Clicked(gtx) {
|
for p.State.clickable.Clicked(gtx) {
|
||||||
t.ChooseSendTarget(p.Parameter.UnitID(), port).Do()
|
t.Params().ChooseSendTarget(p.Parameter.UnitID(), port).Do()
|
||||||
}
|
}
|
||||||
k := Port(p.Theme, p.State)
|
k := Port(p.Theme, p.State)
|
||||||
return k.Layout(gtx)
|
return k.Layout(gtx)
|
||||||
@@ -144,7 +144,7 @@ func (p ParamWidget) Layout(gtx C) D {
|
|||||||
return s.Layout(gtx)
|
return s.Layout(gtx)
|
||||||
case tracker.IDParameter:
|
case tracker.IDParameter:
|
||||||
for p.State.clickable.Clicked(gtx) {
|
for p.State.clickable.Clicked(gtx) {
|
||||||
t.ChooseSendSource(p.Parameter.UnitID()).Do()
|
t.Params().ChooseSendSource(p.Parameter.UnitID()).Do()
|
||||||
}
|
}
|
||||||
btn := Btn(t.Theme, &t.Theme.Button.Text, &p.State.clickable, "Set", p.Parameter.Hint().Label)
|
btn := Btn(t.Theme, &t.Theme.Button.Text, &p.State.clickable, "Set", p.Parameter.Hint().Label)
|
||||||
if p.Disabled {
|
if p.Disabled {
|
||||||
|
|||||||
@@ -75,9 +75,9 @@ func (pp *PatchPanel) Layout(gtx C) D {
|
|||||||
tr := TrackerFromContext(gtx)
|
tr := TrackerFromContext(gtx)
|
||||||
bottom := func(gtx C) D {
|
bottom := func(gtx C) D {
|
||||||
switch {
|
switch {
|
||||||
case tr.InstrComment().Value():
|
case tr.Instrument().Tab().Value() == int(tracker.InstrumentCommentTab):
|
||||||
return pp.instrProps.layout(gtx)
|
return pp.instrProps.layout(gtx)
|
||||||
case tr.InstrPresets().Value():
|
case tr.Instrument().Tab().Value() == int(tracker.InstrumentPresetsTab):
|
||||||
return pp.instrPresets.layout(gtx)
|
return pp.instrPresets.layout(gtx)
|
||||||
default: // editor
|
default: // editor
|
||||||
return pp.instrEditor.layout(gtx)
|
return pp.instrEditor.layout(gtx)
|
||||||
@@ -92,9 +92,9 @@ func (pp *PatchPanel) Layout(gtx C) D {
|
|||||||
|
|
||||||
func (pp *PatchPanel) BottomTags(level int, yield TagYieldFunc) bool {
|
func (pp *PatchPanel) BottomTags(level int, yield TagYieldFunc) bool {
|
||||||
switch {
|
switch {
|
||||||
case pp.InstrComment().Value():
|
case pp.Instrument().Tab().Value() == int(tracker.InstrumentCommentTab):
|
||||||
return pp.instrProps.Tags(level, yield)
|
return pp.instrProps.Tags(level, yield)
|
||||||
case pp.InstrPresets().Value():
|
case pp.Instrument().Tab().Value() == int(tracker.InstrumentPresetsTab):
|
||||||
return pp.instrPresets.Tags(level, yield)
|
return pp.instrPresets.Tags(level, yield)
|
||||||
default: // editor
|
default: // editor
|
||||||
return pp.instrEditor.Tags(level, yield)
|
return pp.instrEditor.Tags(level, yield)
|
||||||
@@ -143,18 +143,18 @@ func MakeInstrumentTools(m *tracker.Model) InstrumentTools {
|
|||||||
func (it *InstrumentTools) Layout(gtx C) D {
|
func (it *InstrumentTools) Layout(gtx C) D {
|
||||||
t := TrackerFromContext(gtx)
|
t := TrackerFromContext(gtx)
|
||||||
it.update(gtx, t)
|
it.update(gtx, t)
|
||||||
editorBtn := TabBtn(t.Model.InstrEditor(), t.Theme, it.EditorTab, "Editor", "")
|
editorBtn := TabBtn(tracker.MakeBool((*editorTab)(t.Model)), t.Theme, it.EditorTab, "Editor", "")
|
||||||
presetsBtn := TabBtn(t.Model.InstrPresets(), t.Theme, it.PresetsTab, "Presets", "")
|
presetsBtn := TabBtn(tracker.MakeBool((*presetsTab)(t.Model)), t.Theme, it.PresetsTab, "Presets", "")
|
||||||
commentBtn := TabBtn(t.Model.InstrComment(), t.Theme, it.CommentTab, "Properties", "")
|
commentBtn := TabBtn(tracker.MakeBool((*commentTab)(t.Model)), t.Theme, it.CommentTab, "Properties", "")
|
||||||
octave := NumUpDown(t.Model.Octave(), t.Theme, t.OctaveNumberInput, "Octave")
|
octave := NumUpDown(t.Note().Octave(), t.Theme, t.OctaveNumberInput, "Octave")
|
||||||
linkInstrTrackBtn := ToggleIconBtn(t.Model.LinkInstrTrack(), t.Theme, it.linkInstrTrackBtn, icons.NotificationSyncDisabled, icons.NotificationSync, it.linkDisabledHint, it.linkEnabledHint)
|
linkInstrTrackBtn := ToggleIconBtn(t.Track().LinkInstrument(), t.Theme, it.linkInstrTrackBtn, icons.NotificationSyncDisabled, icons.NotificationSync, it.linkDisabledHint, it.linkEnabledHint)
|
||||||
instrEnlargedBtn := ToggleIconBtn(t.Model.InstrEnlarged(), t.Theme, it.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, it.enlargeHint, it.shrinkHint)
|
instrEnlargedBtn := ToggleIconBtn(t.Play().TrackerHidden(), t.Theme, it.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, it.enlargeHint, it.shrinkHint)
|
||||||
addInstrumentBtn := ActionIconBtn(t.Model.AddInstrument(), t.Theme, it.newInstrumentBtn, icons.ContentAdd, it.addInstrumentHint)
|
addInstrumentBtn := ActionIconBtn(t.Model.Instrument().Add(), t.Theme, it.newInstrumentBtn, icons.ContentAdd, it.addInstrumentHint)
|
||||||
|
|
||||||
saveInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, it.saveInstrumentBtn, icons.ContentSave, "Save instrument")
|
saveInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, it.saveInstrumentBtn, icons.ContentSave, "Save instrument")
|
||||||
loadInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, it.loadInstrumentBtn, icons.FileFolderOpen, "Load instrument")
|
loadInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, it.loadInstrumentBtn, icons.FileFolderOpen, "Load instrument")
|
||||||
copyInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, it.copyInstrumentBtn, icons.ContentContentCopy, "Copy instrument")
|
copyInstrumentBtn := IconBtn(t.Theme, &t.Theme.IconButton.Enabled, it.copyInstrumentBtn, icons.ContentContentCopy, "Copy instrument")
|
||||||
deleteInstrumentBtn := ActionIconBtn(t.DeleteInstrument(), t.Theme, it.deleteInstrumentBtn, icons.ActionDelete, it.deleteInstrumentHint)
|
deleteInstrumentBtn := ActionIconBtn(t.Instrument().Delete(), t.Theme, it.deleteInstrumentBtn, icons.ActionDelete, it.deleteInstrumentHint)
|
||||||
btns := func(gtx C) D {
|
btns := func(gtx C) D {
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Rigid(layout.Spacer{Width: 6}.Layout),
|
layout.Rigid(layout.Spacer{Width: 6}.Layout),
|
||||||
@@ -177,26 +177,58 @@ func (it *InstrumentTools) Layout(gtx C) D {
|
|||||||
return Surface{Height: 4, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, btns)
|
return Surface{Height: 4, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, btns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
editorTab tracker.Model
|
||||||
|
presetsTab tracker.Model
|
||||||
|
commentTab tracker.Model
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *editorTab) Value() bool {
|
||||||
|
return (*tracker.Model)(e).Instrument().Tab().Value() == int(tracker.InstrumentEditorTab)
|
||||||
|
}
|
||||||
|
func (e *editorTab) SetValue(val bool) {
|
||||||
|
if val {
|
||||||
|
(*tracker.Model)(e).Instrument().Tab().SetValue(int(tracker.InstrumentEditorTab))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *presetsTab) Value() bool {
|
||||||
|
return (*tracker.Model)(p).Instrument().Tab().Value() == int(tracker.InstrumentPresetsTab)
|
||||||
|
}
|
||||||
|
func (p *presetsTab) SetValue(val bool) {
|
||||||
|
if val {
|
||||||
|
(*tracker.Model)(p).Instrument().Tab().SetValue(int(tracker.InstrumentPresetsTab))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (c *commentTab) Value() bool {
|
||||||
|
return (*tracker.Model)(c).Instrument().Tab().Value() == int(tracker.InstrumentCommentTab)
|
||||||
|
}
|
||||||
|
func (c *commentTab) SetValue(val bool) {
|
||||||
|
if val {
|
||||||
|
(*tracker.Model)(c).Instrument().Tab().SetValue(int(tracker.InstrumentCommentTab))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (it *InstrumentTools) update(gtx C, tr *Tracker) {
|
func (it *InstrumentTools) update(gtx C, tr *Tracker) {
|
||||||
for it.copyInstrumentBtn.Clicked(gtx) {
|
for it.copyInstrumentBtn.Clicked(gtx) {
|
||||||
if contents, ok := tr.Instruments().CopyElements(); ok {
|
if contents, ok := tr.Instrument().List().CopyElements(); ok {
|
||||||
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))})
|
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))})
|
||||||
tr.Alerts().Add("Instrument copied to clipboard", tracker.Info)
|
tr.Alerts().Add("Instrument copied to clipboard", tracker.Info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for it.saveInstrumentBtn.Clicked(gtx) {
|
for it.saveInstrumentBtn.Clicked(gtx) {
|
||||||
writer, err := tr.Explorer.CreateFile(tr.InstrumentName().Value() + ".yml")
|
writer, err := tr.Explorer.CreateFile(tr.Instrument().Name().Value() + ".yml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tr.SaveInstrument(writer)
|
tr.Instrument().Write(writer)
|
||||||
}
|
}
|
||||||
for it.loadInstrumentBtn.Clicked(gtx) {
|
for it.loadInstrumentBtn.Clicked(gtx) {
|
||||||
reader, err := tr.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp")
|
reader, err := tr.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tr.LoadInstrument(reader)
|
tr.Instrument().Read(reader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +240,7 @@ func (it *InstrumentTools) Tags(level int, yield TagYieldFunc) bool {
|
|||||||
|
|
||||||
func MakeInstrList(model *tracker.Model) InstrumentList {
|
func MakeInstrList(model *tracker.Model) InstrumentList {
|
||||||
return InstrumentList{
|
return InstrumentList{
|
||||||
instrumentDragList: NewDragList(model.Instruments(), layout.Horizontal),
|
instrumentDragList: NewDragList(model.Instrument().List(), layout.Horizontal),
|
||||||
nameEditor: NewEditor(true, true, text.Middle),
|
nameEditor: NewEditor(true, true, text.Middle),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,7 +253,7 @@ func (il *InstrumentList) Layout(gtx C) D {
|
|||||||
element := func(gtx C, i int) D {
|
element := func(gtx C, i int) D {
|
||||||
grabhandle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, strconv.Itoa(i+1))
|
grabhandle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, strconv.Itoa(i+1))
|
||||||
label := func(gtx C) D {
|
label := func(gtx C) D {
|
||||||
name, level, mute, ok := t.Instrument(i)
|
name, level, mute, ok := t.Instrument().Item(i)
|
||||||
if !ok {
|
if !ok {
|
||||||
labelStyle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, "")
|
labelStyle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, "")
|
||||||
return layout.Center.Layout(gtx, labelStyle.Layout)
|
return layout.Center.Layout(gtx, labelStyle.Layout)
|
||||||
@@ -233,12 +265,12 @@ func (il *InstrumentList) Layout(gtx C) D {
|
|||||||
s.Color = color.NRGBA{R: 255, G: k, B: 255, A: 255}
|
s.Color = color.NRGBA{R: 255, G: k, B: 255, A: 255}
|
||||||
}
|
}
|
||||||
if i == il.instrumentDragList.TrackerList.Selected() {
|
if i == il.instrumentDragList.TrackerList.Selected() {
|
||||||
for il.nameEditor.Update(gtx, t.InstrumentName()) != EditorEventNone {
|
for il.nameEditor.Update(gtx, t.Instrument().Name()) != EditorEventNone {
|
||||||
il.instrumentDragList.Focus()
|
il.instrumentDragList.Focus()
|
||||||
}
|
}
|
||||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||||
return il.nameEditor.Layout(gtx, t.InstrumentName(), t.Theme, &s, "Instr")
|
return il.nameEditor.Layout(gtx, t.Instrument().Name(), t.Theme, &s, "Instr")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if name == "" {
|
if name == "" {
|
||||||
@@ -280,9 +312,9 @@ func (il *InstrumentList) update(gtx C, t *Tracker) {
|
|||||||
case key.NameDownArrow:
|
case key.NameDownArrow:
|
||||||
var tagged Tagged
|
var tagged Tagged
|
||||||
switch {
|
switch {
|
||||||
case t.InstrComment().Value():
|
case t.Instrument().Tab().Value() == int(tracker.InstrumentCommentTab):
|
||||||
tagged = &t.PatchPanel.instrProps
|
tagged = &t.PatchPanel.instrProps
|
||||||
case t.InstrPresets().Value():
|
case t.Instrument().Tab().Value() == int(tracker.InstrumentPresetsTab):
|
||||||
tagged = &t.PatchPanel.instrPresets
|
tagged = &t.PatchPanel.instrPresets
|
||||||
default: // editor
|
default: // editor
|
||||||
tagged = &t.PatchPanel.instrEditor
|
tagged = &t.PatchPanel.instrEditor
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func NewSongPanel(tr *Tracker) *SongPanel {
|
|||||||
RowsPerBeat: NewNumericUpDownState(),
|
RowsPerBeat: NewNumericUpDownState(),
|
||||||
Step: NewNumericUpDownState(),
|
Step: NewNumericUpDownState(),
|
||||||
SongLength: NewNumericUpDownState(),
|
SongLength: NewNumericUpDownState(),
|
||||||
Scope: NewOscilloscope(tr.Model),
|
Scope: NewOscilloscope(),
|
||||||
MenuBar: NewMenuBar(tr),
|
MenuBar: NewMenuBar(tr),
|
||||||
PlayBar: NewPlayBar(),
|
PlayBar: NewPlayBar(),
|
||||||
|
|
||||||
@@ -88,14 +88,14 @@ func NewSongPanel(tr *Tracker) *SongPanel {
|
|||||||
|
|
||||||
func (s *SongPanel) Update(gtx C, t *Tracker) {
|
func (s *SongPanel) Update(gtx C, t *Tracker) {
|
||||||
for s.WeightingTypeBtn.Clicked(gtx) {
|
for s.WeightingTypeBtn.Clicked(gtx) {
|
||||||
t.Model.DetectorWeighting().SetValue((t.DetectorWeighting().Value() + 1) % int(tracker.NumWeightingTypes))
|
t.Model.Detector().Weighting().SetValue((t.Detector().Weighting().Value() + 1) % int(tracker.NumWeightingTypes))
|
||||||
}
|
}
|
||||||
for s.OversamplingBtn.Clicked(gtx) {
|
for s.OversamplingBtn.Clicked(gtx) {
|
||||||
t.Model.Oversampling().SetValue(!t.Oversampling().Value())
|
t.Model.Detector().Oversampling().SetValue(!t.Detector().Oversampling().Value())
|
||||||
}
|
}
|
||||||
for s.SynthBtn.Clicked(gtx) {
|
for s.SynthBtn.Clicked(gtx) {
|
||||||
r := t.Model.SyntherIndex().Range()
|
r := t.Model.Play().SyntherIndex().Range()
|
||||||
t.Model.SyntherIndex().SetValue((t.SyntherIndex().Value()+1)%(r.Max-r.Min+1) + r.Min)
|
t.Model.Play().SyntherIndex().SetValue((t.Play().SyntherIndex().Value()+1)%(r.Max-r.Min+1) + r.Min)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
|||||||
paint.FillShape(gtx.Ops, tr.Theme.SongPanel.Bg, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
|
paint.FillShape(gtx.Ops, tr.Theme.SongPanel.Bg, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
|
||||||
|
|
||||||
var weightingTxt string
|
var weightingTxt string
|
||||||
switch tracker.WeightingType(tr.Model.DetectorWeighting().Value()) {
|
switch tracker.WeightingType(tr.Model.Detector().Weighting().Value()) {
|
||||||
case tracker.KWeighting:
|
case tracker.KWeighting:
|
||||||
weightingTxt = "K-weight (LUFS)"
|
weightingTxt = "K-weight (LUFS)"
|
||||||
case tracker.AWeighting:
|
case tracker.AWeighting:
|
||||||
@@ -128,14 +128,14 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
|||||||
weightingBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.WeightingTypeBtn, weightingTxt, "")
|
weightingBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.WeightingTypeBtn, weightingTxt, "")
|
||||||
|
|
||||||
oversamplingTxt := "Sample peak"
|
oversamplingTxt := "Sample peak"
|
||||||
if tr.Model.Oversampling().Value() {
|
if tr.Model.Detector().Oversampling().Value() {
|
||||||
oversamplingTxt = "True peak"
|
oversamplingTxt = "True peak"
|
||||||
}
|
}
|
||||||
oversamplingBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.OversamplingBtn, oversamplingTxt, "")
|
oversamplingBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.OversamplingBtn, oversamplingTxt, "")
|
||||||
|
|
||||||
cpuSmallLabel := func(gtx C) D {
|
cpuSmallLabel := func(gtx C) D {
|
||||||
var a [vm.MAX_THREADS]sointu.CPULoad
|
var a [vm.MAX_THREADS]sointu.CPULoad
|
||||||
c := tr.Model.CPULoad(a[:])
|
c := tr.Play().CPULoad(a[:])
|
||||||
if c < 1 {
|
if c < 1 {
|
||||||
return D{}
|
return D{}
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
|||||||
cpuEnlargedWidget := func(gtx C) D {
|
cpuEnlargedWidget := func(gtx C) D {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
var a [vm.MAX_THREADS]sointu.CPULoad
|
var a [vm.MAX_THREADS]sointu.CPULoad
|
||||||
c := tr.Model.CPULoad(a[:])
|
c := tr.Play().CPULoad(a[:])
|
||||||
high := false
|
high := false
|
||||||
for i := range c {
|
for i := range c {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@@ -169,35 +169,35 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
|||||||
return cpuLabel.Layout(gtx)
|
return cpuLabel.Layout(gtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
synthBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.SynthBtn, tr.Model.SyntherName(), "")
|
synthBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.SynthBtn, tr.Model.Play().SyntherName(), "")
|
||||||
|
|
||||||
listItem := func(gtx C, index int) D {
|
listItem := func(gtx C, index int) D {
|
||||||
switch index {
|
switch index {
|
||||||
case 0:
|
case 0:
|
||||||
return t.SongSettingsExpander.Layout(gtx, tr.Theme, "Song",
|
return t.SongSettingsExpander.Layout(gtx, tr.Theme, "Song",
|
||||||
func(gtx C) D {
|
func(gtx C) D {
|
||||||
return Label(tr.Theme, &tr.Theme.SongPanel.RowHeader, strconv.Itoa(tr.BPM().Value())+" BPM").Layout(gtx)
|
return Label(tr.Theme, &tr.Theme.SongPanel.RowHeader, strconv.Itoa(tr.Song().BPM().Value())+" BPM").Layout(gtx)
|
||||||
},
|
},
|
||||||
func(gtx C) D {
|
func(gtx C) D {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
bpm := NumUpDown(tr.BPM(), tr.Theme, t.BPM, "BPM")
|
bpm := NumUpDown(tr.Song().BPM(), tr.Theme, t.BPM, "BPM")
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "BPM", bpm.Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "BPM", bpm.Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
songLength := NumUpDown(tr.SongLength(), tr.Theme, t.SongLength, "Song length")
|
songLength := NumUpDown(tr.Song().Length(), tr.Theme, t.SongLength, "Song length")
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Song length", songLength.Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Song length", songLength.Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
rowsPerPattern := NumUpDown(tr.RowsPerPattern(), tr.Theme, t.RowsPerPattern, "Rows per pattern")
|
rowsPerPattern := NumUpDown(tr.Song().RowsPerPattern(), tr.Theme, t.RowsPerPattern, "Rows per pattern")
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Rows per pat", rowsPerPattern.Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Rows per pat", rowsPerPattern.Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
rowsPerBeat := NumUpDown(tr.RowsPerBeat(), tr.Theme, t.RowsPerBeat, "Rows per beat")
|
rowsPerBeat := NumUpDown(tr.Song().RowsPerBeat(), tr.Theme, t.RowsPerBeat, "Rows per beat")
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Rows per beat", rowsPerBeat.Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Rows per beat", rowsPerBeat.Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
step := NumUpDown(tr.Step(), tr.Theme, t.Step, "Cursor step")
|
step := NumUpDown(tr.Note().Step(), tr.Theme, t.Step, "Cursor step")
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Cursor step", step.Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Cursor step", step.Layout)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -214,25 +214,25 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
|||||||
case 2:
|
case 2:
|
||||||
return t.LoudnessExpander.Layout(gtx, tr.Theme, "Loudness",
|
return t.LoudnessExpander.Layout(gtx, tr.Theme, "Loudness",
|
||||||
func(gtx C) D {
|
func(gtx C) D {
|
||||||
loudness := tr.Model.DetectorResult().Loudness[tracker.LoudnessShortTerm]
|
loudness := tr.Model.Detector().Result().Loudness[tracker.LoudnessShortTerm]
|
||||||
return dbLabel(tr.Theme, loudness).Layout(gtx)
|
return dbLabel(tr.Theme, loudness).Layout(gtx)
|
||||||
},
|
},
|
||||||
func(gtx C) D {
|
func(gtx C) D {
|
||||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Momentary", dbLabel(tr.Theme, tr.Model.DetectorResult().Loudness[tracker.LoudnessMomentary]).Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Momentary", dbLabel(tr.Theme, tr.Model.Detector().Result().Loudness[tracker.LoudnessMomentary]).Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Short term", dbLabel(tr.Theme, tr.Model.DetectorResult().Loudness[tracker.LoudnessShortTerm]).Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Short term", dbLabel(tr.Theme, tr.Model.Detector().Result().Loudness[tracker.LoudnessShortTerm]).Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Integrated", dbLabel(tr.Theme, tr.Model.DetectorResult().Loudness[tracker.LoudnessIntegrated]).Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Integrated", dbLabel(tr.Theme, tr.Model.Detector().Result().Loudness[tracker.LoudnessIntegrated]).Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Max. momentary", dbLabel(tr.Theme, tr.Model.DetectorResult().Loudness[tracker.LoudnessMaxMomentary]).Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Max. momentary", dbLabel(tr.Theme, tr.Model.Detector().Result().Loudness[tracker.LoudnessMaxMomentary]).Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Max. short term", dbLabel(tr.Theme, tr.Model.DetectorResult().Loudness[tracker.LoudnessMaxShortTerm]).Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Max. short term", dbLabel(tr.Theme, tr.Model.Detector().Result().Loudness[tracker.LoudnessMaxShortTerm]).Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
gtx.Constraints.Min.X = 0
|
gtx.Constraints.Min.X = 0
|
||||||
@@ -244,23 +244,23 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
|||||||
case 3:
|
case 3:
|
||||||
return t.PeakExpander.Layout(gtx, tr.Theme, "Peaks",
|
return t.PeakExpander.Layout(gtx, tr.Theme, "Peaks",
|
||||||
func(gtx C) D {
|
func(gtx C) D {
|
||||||
maxPeak := max(tr.Model.DetectorResult().Peaks[tracker.PeakShortTerm][0], tr.Model.DetectorResult().Peaks[tracker.PeakShortTerm][1])
|
maxPeak := max(tr.Model.Detector().Result().Peaks[tracker.PeakShortTerm][0], tr.Model.Detector().Result().Peaks[tracker.PeakShortTerm][1])
|
||||||
return dbLabel(tr.Theme, maxPeak).Layout(gtx)
|
return dbLabel(tr.Theme, maxPeak).Layout(gtx)
|
||||||
},
|
},
|
||||||
func(gtx C) D {
|
func(gtx C) D {
|
||||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx,
|
||||||
// no need to show momentary peak, it does not have too much meaning
|
// no need to show momentary peak, it does not have too much meaning
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Short term L", dbLabel(tr.Theme, tr.Model.DetectorResult().Peaks[tracker.PeakShortTerm][0]).Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Short term L", dbLabel(tr.Theme, tr.Model.Detector().Result().Peaks[tracker.PeakShortTerm][0]).Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Short term R", dbLabel(tr.Theme, tr.Model.DetectorResult().Peaks[tracker.PeakShortTerm][1]).Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Short term R", dbLabel(tr.Theme, tr.Model.Detector().Result().Peaks[tracker.PeakShortTerm][1]).Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Integrated L", dbLabel(tr.Theme, tr.Model.DetectorResult().Peaks[tracker.PeakIntegrated][0]).Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Integrated L", dbLabel(tr.Theme, tr.Model.Detector().Result().Peaks[tracker.PeakIntegrated][0]).Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Integrated R", dbLabel(tr.Theme, tr.Model.DetectorResult().Peaks[tracker.PeakIntegrated][1]).Layout)
|
return layoutSongOptionRow(gtx, tr.Theme, "Integrated R", dbLabel(tr.Theme, tr.Model.Detector().Result().Peaks[tracker.PeakIntegrated][1]).Layout)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
gtx.Constraints.Min.X = 0
|
gtx.Constraints.Min.X = 0
|
||||||
@@ -270,7 +270,7 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
case 4:
|
case 4:
|
||||||
scope := Scope(tr.Theme, tr.Model.SignalAnalyzer(), t.Scope)
|
scope := Scope(tr.Theme, t.Scope)
|
||||||
scopeScaleBar := func(gtx C) D {
|
scopeScaleBar := func(gtx C) D {
|
||||||
return t.ScopeScaleBar.Layout(gtx, scope.Layout)
|
return t.ScopeScaleBar.Layout(gtx, scope.Layout)
|
||||||
}
|
}
|
||||||
@@ -289,7 +289,7 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
|||||||
gtx.Constraints.Min = gtx.Constraints.Max
|
gtx.Constraints.Min = gtx.Constraints.Max
|
||||||
dims := t.List.Layout(gtx, 7, listItem)
|
dims := t.List.Layout(gtx, 7, listItem)
|
||||||
t.ScrollBar.Layout(gtx, &tr.Theme.SongPanel.ScrollBar, 7, &t.List.Position)
|
t.ScrollBar.Layout(gtx, &tr.Theme.SongPanel.ScrollBar, 7, &t.List.Position)
|
||||||
tr.SpecAnEnabled().SetValue(t.SpectrumExpander.Expanded)
|
tr.Spectrum().Enabled().SetValue(t.SpectrumExpander.Expanded)
|
||||||
return dims
|
return dims
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,9 +478,9 @@ func NewMenuBar(tr *Tracker) *MenuBar {
|
|||||||
PanicBtn: new(Clickable),
|
PanicBtn: new(Clickable),
|
||||||
panicHint: makeHint("Panic", " (%s)", "PanicToggle"),
|
panicHint: makeHint("Panic", " (%s)", "PanicToggle"),
|
||||||
}
|
}
|
||||||
for input := range tr.MIDI.InputDevices {
|
for input := range tr.MIDI().InputDevices {
|
||||||
ret.midiMenuItems = append(ret.midiMenuItems,
|
ret.midiMenuItems = append(ret.midiMenuItems,
|
||||||
MenuItem(tr.SelectMidiInput(input), input, "", icons.ImageControlPoint),
|
MenuItem(tr.MIDI().Open(input), input, "", icons.ImageControlPoint),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
@@ -495,11 +495,11 @@ func (t *MenuBar) Layout(gtx C) D {
|
|||||||
fileBtn := MenuBtn(tr.Theme, &t.MenuStates[0], &t.Clickables[0], "File")
|
fileBtn := MenuBtn(tr.Theme, &t.MenuStates[0], &t.Clickables[0], "File")
|
||||||
fileFC := layout.Rigid(func(gtx C) D {
|
fileFC := layout.Rigid(func(gtx C) D {
|
||||||
items := [...]ActionMenuItem{
|
items := [...]ActionMenuItem{
|
||||||
MenuItem(tr.NewSong(), "New Song", keyActionMap["NewSong"], icons.ContentClear),
|
MenuItem(tr.Song().New(), "New Song", keyActionMap["NewSong"], icons.ContentClear),
|
||||||
MenuItem(tr.OpenSong(), "Open Song", keyActionMap["OpenSong"], icons.FileFolder),
|
MenuItem(tr.Song().Open(), "Open Song", keyActionMap["OpenSong"], icons.FileFolder),
|
||||||
MenuItem(tr.SaveSong(), "Save Song", keyActionMap["SaveSong"], icons.ContentSave),
|
MenuItem(tr.Song().Save(), "Save Song", keyActionMap["SaveSong"], icons.ContentSave),
|
||||||
MenuItem(tr.SaveSongAs(), "Save Song As...", keyActionMap["SaveSongAs"], icons.ContentSave),
|
MenuItem(tr.Song().SaveAs(), "Save Song As...", keyActionMap["SaveSongAs"], icons.ContentSave),
|
||||||
MenuItem(tr.Export(), "Export Wav...", keyActionMap["ExportWav"], icons.ImageAudiotrack),
|
MenuItem(tr.Song().Export(), "Export Wav...", keyActionMap["ExportWav"], icons.ImageAudiotrack),
|
||||||
MenuItem(tr.RequestQuit(), "Quit", keyActionMap["Quit"], icons.ActionExitToApp),
|
MenuItem(tr.RequestQuit(), "Quit", keyActionMap["Quit"], icons.ActionExitToApp),
|
||||||
}
|
}
|
||||||
if !canQuit {
|
if !canQuit {
|
||||||
@@ -510,9 +510,9 @@ func (t *MenuBar) Layout(gtx C) D {
|
|||||||
editBtn := MenuBtn(tr.Theme, &t.MenuStates[1], &t.Clickables[1], "Edit")
|
editBtn := MenuBtn(tr.Theme, &t.MenuStates[1], &t.Clickables[1], "Edit")
|
||||||
editFC := layout.Rigid(func(gtx C) D {
|
editFC := layout.Rigid(func(gtx C) D {
|
||||||
return editBtn.Layout(gtx,
|
return editBtn.Layout(gtx,
|
||||||
MenuItem(tr.Undo(), "Undo", keyActionMap["Undo"], icons.ContentUndo),
|
MenuItem(tr.History().Undo(), "Undo", keyActionMap["Undo"], icons.ContentUndo),
|
||||||
MenuItem(tr.Redo(), "Redo", keyActionMap["Redo"], icons.ContentRedo),
|
MenuItem(tr.History().Redo(), "Redo", keyActionMap["Redo"], icons.ContentRedo),
|
||||||
MenuItem(tr.RemoveUnused(), "Remove unused data", keyActionMap["RemoveUnused"], icons.ImageCrop),
|
MenuItem(tr.Order().RemoveUnusedPatterns(), "Remove unused data", keyActionMap["RemoveUnused"], icons.ImageCrop),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
midiBtn := MenuBtn(tr.Theme, &t.MenuStates[2], &t.Clickables[2], "MIDI")
|
midiBtn := MenuBtn(tr.Theme, &t.MenuStates[2], &t.Clickables[2], "MIDI")
|
||||||
@@ -527,8 +527,8 @@ func (t *MenuBar) Layout(gtx C) D {
|
|||||||
MenuItem(tr.ReportBug(), "Report bug", keyActionMap["ReportBug"], icons.ActionBugReport),
|
MenuItem(tr.ReportBug(), "Report bug", keyActionMap["ReportBug"], icons.ActionBugReport),
|
||||||
MenuItem(tr.ShowLicense(), "License", keyActionMap["ShowLicense"], icons.ActionCopyright))
|
MenuItem(tr.ShowLicense(), "License", keyActionMap["ShowLicense"], icons.ActionCopyright))
|
||||||
})
|
})
|
||||||
panicBtn := ToggleIconBtn(tr.Panic(), tr.Theme, t.PanicBtn, icons.AlertErrorOutline, icons.AlertError, t.panicHint, t.panicHint)
|
panicBtn := ToggleIconBtn(tr.Play().Panicked(), tr.Theme, t.PanicBtn, icons.AlertErrorOutline, icons.AlertError, t.panicHint, t.panicHint)
|
||||||
if tr.Panic().Value() {
|
if tr.Play().Panicked().Value() {
|
||||||
panicBtn.Style = &tr.Theme.IconButton.Error
|
panicBtn.Style = &tr.Theme.IconButton.Error
|
||||||
}
|
}
|
||||||
panicFC := layout.Flexed(1, func(gtx C) D { return layout.E.Layout(gtx, panicBtn.Layout) })
|
panicFC := layout.Flexed(1, func(gtx C) D { return layout.E.Layout(gtx, panicBtn.Layout) })
|
||||||
@@ -574,11 +574,11 @@ func NewPlayBar() *PlayBar {
|
|||||||
|
|
||||||
func (pb *PlayBar) Layout(gtx C) D {
|
func (pb *PlayBar) Layout(gtx C) D {
|
||||||
tr := TrackerFromContext(gtx)
|
tr := TrackerFromContext(gtx)
|
||||||
playBtn := ToggleIconBtn(tr.Playing(), tr.Theme, pb.PlayingBtn, icons.AVPlayArrow, icons.AVStop, pb.playHint, pb.stopHint)
|
playBtn := ToggleIconBtn(tr.Play().Started(), tr.Theme, pb.PlayingBtn, icons.AVPlayArrow, icons.AVStop, pb.playHint, pb.stopHint)
|
||||||
rewindBtn := ActionIconBtn(tr.PlaySongStart(), tr.Theme, pb.RewindBtn, icons.AVFastRewind, pb.rewindHint)
|
rewindBtn := ActionIconBtn(tr.Play().FromBeginning(), tr.Theme, pb.RewindBtn, icons.AVFastRewind, pb.rewindHint)
|
||||||
recordBtn := ToggleIconBtn(tr.IsRecording(), tr.Theme, pb.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, pb.recordHint, pb.stopRecordHint)
|
recordBtn := ToggleIconBtn(tr.Play().IsRecording(), tr.Theme, pb.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, pb.recordHint, pb.stopRecordHint)
|
||||||
followBtn := ToggleIconBtn(tr.Follow(), tr.Theme, pb.FollowBtn, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, pb.followOffHint, pb.followOnHint)
|
followBtn := ToggleIconBtn(tr.Play().IsFollowing(), tr.Theme, pb.FollowBtn, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, pb.followOffHint, pb.followOnHint)
|
||||||
loopBtn := ToggleIconBtn(tr.LoopToggle(), tr.Theme, pb.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, pb.loopOffHint, pb.loopOnHint)
|
loopBtn := ToggleIconBtn(tr.Play().IsLooping(), tr.Theme, pb.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, pb.loopOffHint, pb.loopOnHint)
|
||||||
|
|
||||||
return Surface{Height: 4}.Layout(gtx, func(gtx C) D {
|
return Surface{Height: 4}.Layout(gtx, func(gtx C) D {
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
|
|||||||
@@ -40,29 +40,29 @@ func (s *SpectrumState) Layout(gtx C) D {
|
|||||||
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout
|
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout
|
||||||
|
|
||||||
var chnModeTxt string = "???"
|
var chnModeTxt string = "???"
|
||||||
switch tracker.SpecChnMode(t.Model.SpecAnChannelsInt().Value()) {
|
switch tracker.SpecChnMode(t.Model.Spectrum().Channels().Value()) {
|
||||||
case tracker.SpecChnModeSum:
|
case tracker.SpecChnModeSum:
|
||||||
chnModeTxt = "Sum"
|
chnModeTxt = "Sum"
|
||||||
case tracker.SpecChnModeSeparate:
|
case tracker.SpecChnModeSeparate:
|
||||||
chnModeTxt = "Separate"
|
chnModeTxt = "Separate"
|
||||||
}
|
}
|
||||||
|
|
||||||
resolution := NumUpDown(t.Model.SpecAnResolution(), t.Theme, s.resolutionNumber, "Resolution")
|
resolution := NumUpDown(t.Model.Spectrum().Resolution(), t.Theme, s.resolutionNumber, "Resolution")
|
||||||
chnModeBtn := Btn(t.Theme, &t.Theme.Button.Text, s.chnModeBtn, chnModeTxt, "Channel mode")
|
chnModeBtn := Btn(t.Theme, &t.Theme.Button.Text, s.chnModeBtn, chnModeTxt, "Channel mode")
|
||||||
speed := NumUpDown(t.Model.SpecAnSpeed(), t.Theme, s.speed, "Speed")
|
speed := NumUpDown(t.Model.Spectrum().Speed(), t.Theme, s.speed, "Speed")
|
||||||
|
|
||||||
numchns := 0
|
numchns := 0
|
||||||
speclen := len(t.Model.Spectrum()[0])
|
speclen := len(t.Model.Spectrum().Result()[0])
|
||||||
if speclen > 0 {
|
if speclen > 0 {
|
||||||
numchns = 1
|
numchns = 1
|
||||||
if len(t.Model.Spectrum()[1]) == speclen {
|
if len(t.Model.Spectrum().Result()[1]) == speclen {
|
||||||
numchns = 2
|
numchns = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Flexed(1, func(gtx C) D {
|
layout.Flexed(1, func(gtx C) D {
|
||||||
biquad, biquadok := t.Model.BiquadCoeffs()
|
biquad, biquadok := t.Model.Spectrum().BiquadCoeffs()
|
||||||
data := func(chn int, xr plotRange) (yr plotRange, ok bool) {
|
data := func(chn int, xr plotRange) (yr plotRange, ok bool) {
|
||||||
if chn == 2 {
|
if chn == 2 {
|
||||||
if xr.a >= 0 {
|
if xr.a >= 0 {
|
||||||
@@ -88,16 +88,16 @@ func (s *SpectrumState) Layout(gtx C) D {
|
|||||||
y2 := float32(math.Inf(+1))
|
y2 := float32(math.Inf(+1))
|
||||||
switch {
|
switch {
|
||||||
case x2 <= x1+1 && x2 < speclen-1: // perform smoothstep interpolation when we are overlapping only a few bins
|
case x2 <= x1+1 && x2 < speclen-1: // perform smoothstep interpolation when we are overlapping only a few bins
|
||||||
l := t.Model.Spectrum()[chn][x1]
|
l := t.Model.Spectrum().Result()[chn][x1]
|
||||||
r := t.Model.Spectrum()[chn][x1+1]
|
r := t.Model.Spectrum().Result()[chn][x1+1]
|
||||||
y1 = smoothInterpolate(l, r, float32(f1))
|
y1 = smoothInterpolate(l, r, float32(f1))
|
||||||
l = t.Model.Spectrum()[chn][x2]
|
l = t.Model.Spectrum().Result()[chn][x2]
|
||||||
r = t.Model.Spectrum()[chn][x2+1]
|
r = t.Model.Spectrum().Result()[chn][x2+1]
|
||||||
y2 = smoothInterpolate(l, r, float32(f2))
|
y2 = smoothInterpolate(l, r, float32(f2))
|
||||||
y1, y2 = max(y1, y2), min(y1, y2)
|
y1, y2 = max(y1, y2), min(y1, y2)
|
||||||
default:
|
default:
|
||||||
for i := x1; i <= x2; i++ {
|
for i := x1; i <= x2; i++ {
|
||||||
sample := t.Model.Spectrum()[chn][i]
|
sample := t.Model.Spectrum().Result()[chn][i]
|
||||||
y1 = max(y1, sample)
|
y1 = max(y1, sample)
|
||||||
y2 = min(y2, sample)
|
y2 = min(y2, sample)
|
||||||
}
|
}
|
||||||
@@ -210,8 +210,8 @@ func nextPowerOfTwo(v int) int {
|
|||||||
func (s *SpectrumState) Update(gtx C) {
|
func (s *SpectrumState) Update(gtx C) {
|
||||||
t := TrackerFromContext(gtx)
|
t := TrackerFromContext(gtx)
|
||||||
for s.chnModeBtn.Clicked(gtx) {
|
for s.chnModeBtn.Clicked(gtx) {
|
||||||
t.Model.SpecAnChannelsInt().SetValue((t.SpecAnChannelsInt().Value() + 1) % int(tracker.NumSpecChnModes))
|
t.Model.Spectrum().Channels().SetValue((t.Model.Spectrum().Channels().Value() + 1) % int(tracker.NumSpecChnModes))
|
||||||
}
|
}
|
||||||
s.resolutionNumber.Update(gtx, t.Model.SpecAnResolution())
|
s.resolutionNumber.Update(gtx, t.Model.Spectrum().Resolution())
|
||||||
s.speed.Update(gtx, t.Model.SpecAnSpeed())
|
s.speed.Update(gtx, t.Model.Spectrum().Speed())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func NewTracker(model *tracker.Model) *Tracker {
|
|||||||
|
|
||||||
Model: model,
|
Model: model,
|
||||||
|
|
||||||
filePathString: model.FilePath(),
|
filePathString: model.Song().FilePath(),
|
||||||
}
|
}
|
||||||
t.SongPanel = NewSongPanel(t)
|
t.SongPanel = NewSongPanel(t)
|
||||||
t.KeyNoteMap = MakeKeyboard[key.Name](model.Broker())
|
t.KeyNoteMap = MakeKeyboard[key.Name](model.Broker())
|
||||||
@@ -185,12 +185,12 @@ func (t *Tracker) Main() {
|
|||||||
}
|
}
|
||||||
acks <- struct{}{}
|
acks <- struct{}{}
|
||||||
case <-recoveryTicker.C:
|
case <-recoveryTicker.C:
|
||||||
t.SaveRecovery()
|
t.History().SaveRecovery()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recoveryTicker.Stop()
|
recoveryTicker.Stop()
|
||||||
t.SaveRecovery()
|
t.History().SaveRecovery()
|
||||||
close(t.Broker().FinishedGUI)
|
close(t.Broker().FinishedGUI)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ func (t *Tracker) Layout(gtx layout.Context) {
|
|||||||
paint.Fill(gtx.Ops, t.Theme.Material.Bg)
|
paint.Fill(gtx.Ops, t.Theme.Material.Bg)
|
||||||
event.Op(gtx.Ops, t) // area for capturing scroll events
|
event.Op(gtx.Ops, t) // area for capturing scroll events
|
||||||
|
|
||||||
if t.InstrEnlarged().Value() {
|
if t.Play().TrackerHidden().Value() {
|
||||||
t.layoutTop(gtx)
|
t.layoutTop(gtx)
|
||||||
} else {
|
} else {
|
||||||
t.VerticalSplit.Layout(gtx,
|
t.VerticalSplit.Layout(gtx,
|
||||||
@@ -263,14 +263,14 @@ func (t *Tracker) Layout(gtx layout.Context) {
|
|||||||
case key.Event:
|
case key.Event:
|
||||||
t.KeyEvent(e, gtx)
|
t.KeyEvent(e, gtx)
|
||||||
case transfer.DataEvent:
|
case transfer.DataEvent:
|
||||||
t.ReadSong(e.Open())
|
t.Song().Read(e.Open())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if no-one else handled the note events, we handle them here
|
// if no-one else handled the note events, we handle them here
|
||||||
for len(t.noteEvents) > 0 {
|
for len(t.noteEvents) > 0 {
|
||||||
ev := t.noteEvents[0]
|
ev := t.noteEvents[0]
|
||||||
ev.IsTrack = false
|
ev.IsTrack = false
|
||||||
ev.Channel = t.Model.Instruments().Selected()
|
ev.Channel = t.Model.Instrument().List().Selected()
|
||||||
ev.Source = t
|
ev.Source = t
|
||||||
copy(t.noteEvents, t.noteEvents[1:])
|
copy(t.noteEvents, t.noteEvents[1:])
|
||||||
t.noteEvents = t.noteEvents[:len(t.noteEvents)-1]
|
t.noteEvents = t.noteEvents[:len(t.noteEvents)-1]
|
||||||
@@ -285,49 +285,49 @@ func (t *Tracker) showDialog(gtx C) {
|
|||||||
switch t.Dialog() {
|
switch t.Dialog() {
|
||||||
case tracker.NewSongChanges, tracker.OpenSongChanges, tracker.QuitChanges:
|
case tracker.NewSongChanges, tracker.OpenSongChanges, tracker.QuitChanges:
|
||||||
dialog := MakeDialog(t.Theme, t.DialogState, "Save changes to song?", "Your changes will be lost if you don't save them.",
|
dialog := MakeDialog(t.Theme, t.DialogState, "Save changes to song?", "Your changes will be lost if you don't save them.",
|
||||||
DialogBtn("Save", t.SaveSong()),
|
DialogBtn("Save", t.Song().Save()),
|
||||||
DialogBtn("Don't save", t.DiscardSong()),
|
DialogBtn("Don't save", t.Song().Discard()),
|
||||||
DialogBtn("Cancel", t.Cancel()),
|
DialogBtn("Cancel", t.CancelDialog()),
|
||||||
)
|
)
|
||||||
dialog.Layout(gtx)
|
dialog.Layout(gtx)
|
||||||
case tracker.Export:
|
case tracker.Export:
|
||||||
dialog := MakeDialog(t.Theme, t.DialogState, "Export format", "Choose the sample format for the exported .wav file.",
|
dialog := MakeDialog(t.Theme, t.DialogState, "Export format", "Choose the sample format for the exported .wav file.",
|
||||||
DialogBtn("Int16", t.ExportInt16()),
|
DialogBtn("Int16", t.Song().ExportInt16()),
|
||||||
DialogBtn("Float32", t.ExportFloat()),
|
DialogBtn("Float32", t.Song().ExportFloat()),
|
||||||
DialogBtn("Cancel", t.Cancel()),
|
DialogBtn("Cancel", t.CancelDialog()),
|
||||||
)
|
)
|
||||||
dialog.Layout(gtx)
|
dialog.Layout(gtx)
|
||||||
case tracker.OpenSongOpenExplorer:
|
case tracker.OpenSongOpenExplorer:
|
||||||
t.explorerChooseFile(t.ReadSong, ".yml", ".json")
|
t.explorerChooseFile(t.Song().Read, ".yml", ".json")
|
||||||
case tracker.NewSongSaveExplorer, tracker.OpenSongSaveExplorer, tracker.QuitSaveExplorer, tracker.SaveAsExplorer:
|
case tracker.NewSongSaveExplorer, tracker.OpenSongSaveExplorer, tracker.QuitSaveExplorer, tracker.SaveAsExplorer:
|
||||||
filename := t.filePathString.Value()
|
filename := t.filePathString.Value()
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
filename = "song.yml"
|
filename = "song.yml"
|
||||||
}
|
}
|
||||||
t.explorerCreateFile(t.WriteSong, filename)
|
t.explorerCreateFile(t.Song().Write, filename)
|
||||||
case tracker.ExportFloatExplorer, tracker.ExportInt16Explorer:
|
case tracker.ExportFloatExplorer, tracker.ExportInt16Explorer:
|
||||||
filename := "song.wav"
|
filename := "song.wav"
|
||||||
if p := t.filePathString.Value(); p != "" {
|
if p := t.filePathString.Value(); p != "" {
|
||||||
filename = p[:len(p)-len(filepath.Ext(p))] + ".wav"
|
filename = p[:len(p)-len(filepath.Ext(p))] + ".wav"
|
||||||
}
|
}
|
||||||
t.explorerCreateFile(func(wc io.WriteCloser) {
|
t.explorerCreateFile(func(wc io.WriteCloser) {
|
||||||
t.WriteWav(wc, t.Dialog() == tracker.ExportInt16Explorer)
|
t.Song().WriteWav(wc, t.Dialog() == tracker.ExportInt16Explorer)
|
||||||
}, filename)
|
}, filename)
|
||||||
case tracker.License:
|
case tracker.License:
|
||||||
dialog := MakeDialog(t.Theme, t.DialogState, "License", sointu.License,
|
dialog := MakeDialog(t.Theme, t.DialogState, "License", sointu.License,
|
||||||
DialogBtn("Close", t.Cancel()),
|
DialogBtn("Close", t.CancelDialog()),
|
||||||
)
|
)
|
||||||
dialog.Layout(gtx)
|
dialog.Layout(gtx)
|
||||||
case tracker.DeleteUserPresetDialog:
|
case tracker.DeleteUserPresetDialog:
|
||||||
dialog := MakeDialog(t.Theme, t.DialogState, "Delete user preset?", "Are you sure you want to delete the selected user preset?\nThis action cannot be undone.",
|
dialog := MakeDialog(t.Theme, t.DialogState, "Delete user preset?", "Are you sure you want to delete the selected user preset?\nThis action cannot be undone.",
|
||||||
DialogBtn("Delete", t.DeleteUserPreset()),
|
DialogBtn("Delete", t.Preset().ConfirmDelete()),
|
||||||
DialogBtn("Cancel", t.Cancel()),
|
DialogBtn("Cancel", t.CancelDialog()),
|
||||||
)
|
)
|
||||||
dialog.Layout(gtx)
|
dialog.Layout(gtx)
|
||||||
case tracker.OverwriteUserPresetDialog:
|
case tracker.OverwriteUserPresetDialog:
|
||||||
dialog := MakeDialog(t.Theme, t.DialogState, "Overwrite user preset?", "Are you sure you want to overwrite the existing user preset with the same name?",
|
dialog := MakeDialog(t.Theme, t.DialogState, "Overwrite user preset?", "Are you sure you want to overwrite the existing user preset with the same name?",
|
||||||
DialogBtn("Save", t.OverwriteUserPreset()),
|
DialogBtn("Save", t.Preset().Overwrite()),
|
||||||
DialogBtn("Cancel", t.Cancel()),
|
DialogBtn("Cancel", t.CancelDialog()),
|
||||||
)
|
)
|
||||||
dialog.Layout(gtx)
|
dialog.Layout(gtx)
|
||||||
}
|
}
|
||||||
@@ -342,7 +342,7 @@ func (t *Tracker) explorerChooseFile(success func(io.ReadCloser), extensions ...
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
success(file)
|
success(file)
|
||||||
} else {
|
} else {
|
||||||
t.Cancel().Do()
|
t.CancelDialog().Do()
|
||||||
if err != explorer.ErrUserDecline {
|
if err != explorer.ErrUserDecline {
|
||||||
t.Alerts().Add(err.Error(), tracker.Error)
|
t.Alerts().Add(err.Error(), tracker.Error)
|
||||||
}
|
}
|
||||||
@@ -360,7 +360,7 @@ func (t *Tracker) explorerCreateFile(success func(io.WriteCloser), filename stri
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
success(file)
|
success(file)
|
||||||
} else {
|
} else {
|
||||||
t.Cancel().Do()
|
t.CancelDialog().Do()
|
||||||
if err != explorer.ErrUserDecline {
|
if err != explorer.ErrUserDecline {
|
||||||
t.Alerts().Add(err.Error(), tracker.Error)
|
t.Alerts().Add(err.Error(), tracker.Error)
|
||||||
}
|
}
|
||||||
@@ -416,7 +416,7 @@ func (t *Tracker) openUrl(url string) {
|
|||||||
|
|
||||||
func (t *Tracker) Tags(curLevel int, yield TagYieldFunc) bool {
|
func (t *Tracker) Tags(curLevel int, yield TagYieldFunc) bool {
|
||||||
ret := t.PatchPanel.Tags(curLevel+1, yield)
|
ret := t.PatchPanel.Tags(curLevel+1, yield)
|
||||||
if !t.InstrEnlarged().Value() {
|
if !t.Play().TrackerHidden().Value() {
|
||||||
ret = ret && t.OrderEditor.Tags(curLevel+1, yield) &&
|
ret = ret && t.OrderEditor.Tags(curLevel+1, yield) &&
|
||||||
t.TrackEditor.Tags(curLevel+1, yield)
|
t.TrackEditor.Tags(curLevel+1, yield)
|
||||||
}
|
}
|
||||||
|
|||||||
118
tracker/history.go
Normal file
118
tracker/history.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// History returns the History view of the model, containing methods to manipulate
|
||||||
|
// the undo/redo history and saving recovery files.
|
||||||
|
func (m *Model) History() *HistoryModel { return (*HistoryModel)(m) }
|
||||||
|
|
||||||
|
type HistoryModel Model
|
||||||
|
|
||||||
|
// Undo returns an Action to undo the last change.
|
||||||
|
func (m *HistoryModel) Undo() Action { return MakeAction((*historyUndo)(m)) }
|
||||||
|
|
||||||
|
type historyUndo HistoryModel
|
||||||
|
|
||||||
|
func (m *historyUndo) Enabled() bool { return len((*Model)(m).undoStack) > 0 }
|
||||||
|
func (m *historyUndo) Do() {
|
||||||
|
m.redoStack = append(m.redoStack, m.d.Copy())
|
||||||
|
if len(m.redoStack) >= maxUndo {
|
||||||
|
copy(m.redoStack, m.redoStack[len(m.redoStack)-maxUndo:])
|
||||||
|
m.redoStack = m.redoStack[:maxUndo]
|
||||||
|
}
|
||||||
|
m.d = m.undoStack[len(m.undoStack)-1]
|
||||||
|
m.undoStack = m.undoStack[:len(m.undoStack)-1]
|
||||||
|
m.prevUndoKind = ""
|
||||||
|
(*Model)(m).updateDeriveData(SongChange)
|
||||||
|
TrySend(m.broker.ToPlayer, any(m.d.Song.Copy()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redo returns an Action to redo the last undone change.
|
||||||
|
func (m *HistoryModel) Redo() Action { return MakeAction((*historyRedo)(m)) }
|
||||||
|
|
||||||
|
type historyRedo HistoryModel
|
||||||
|
|
||||||
|
func (m *historyRedo) Enabled() bool { return len((*Model)(m).redoStack) > 0 }
|
||||||
|
func (m *historyRedo) Do() {
|
||||||
|
m.undoStack = append(m.undoStack, m.d.Copy())
|
||||||
|
if len(m.undoStack) >= maxUndo {
|
||||||
|
copy(m.undoStack, m.undoStack[len(m.undoStack)-maxUndo:])
|
||||||
|
m.undoStack = m.undoStack[:maxUndo]
|
||||||
|
}
|
||||||
|
m.d = m.redoStack[len(m.redoStack)-1]
|
||||||
|
m.redoStack = m.redoStack[:len(m.redoStack)-1]
|
||||||
|
m.prevUndoKind = ""
|
||||||
|
(*Model)(m).updateDeriveData(SongChange)
|
||||||
|
TrySend(m.broker.ToPlayer, any(m.d.Song.Copy()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalRecovery marshals the current model data to a byte slice for recovery
|
||||||
|
// saving.
|
||||||
|
func (m *HistoryModel) MarshalRecovery() []byte {
|
||||||
|
out, err := json.Marshal(m.d)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if m.d.RecoveryFilePath != "" {
|
||||||
|
os.Remove(m.d.RecoveryFilePath)
|
||||||
|
}
|
||||||
|
m.d.ChangedSinceRecovery = false
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveRecovery saves the current model data to the recovery file on disk if
|
||||||
|
// there are unsaved changes.
|
||||||
|
func (m *HistoryModel) SaveRecovery() error {
|
||||||
|
if !m.d.ChangedSinceRecovery {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if m.d.RecoveryFilePath == "" {
|
||||||
|
return errors.New("no backup file path")
|
||||||
|
}
|
||||||
|
out, err := json.Marshal(m.d)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not marshal recovery data: %w", err)
|
||||||
|
}
|
||||||
|
dir := filepath.Dir(m.d.RecoveryFilePath)
|
||||||
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
|
os.MkdirAll(dir, os.ModePerm)
|
||||||
|
}
|
||||||
|
file, err := os.Create(m.d.RecoveryFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create recovery file: %w", err)
|
||||||
|
}
|
||||||
|
_, err = file.Write(out)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not write recovery file: %w", err)
|
||||||
|
}
|
||||||
|
m.d.ChangedSinceRecovery = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalRecovery unmarshals the model data from a byte slice, then checking
|
||||||
|
// if a recovery file exists on disk and loading it instead.
|
||||||
|
func (m *HistoryModel) UnmarshalRecovery(bytes []byte) {
|
||||||
|
var data modelData
|
||||||
|
err := json.Unmarshal(bytes, &data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.d = data
|
||||||
|
if m.d.RecoveryFilePath != "" { // check if there's a recovery file on disk and load it instead
|
||||||
|
if bytes2, err := os.ReadFile(m.d.RecoveryFilePath); err == nil {
|
||||||
|
var data modelData
|
||||||
|
if json.Unmarshal(bytes2, &data) == nil {
|
||||||
|
m.d = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.d.ChangedSinceRecovery = false
|
||||||
|
TrySend(m.broker.ToPlayer, any(m.d.Song.Copy()))
|
||||||
|
(*Model)(m).updateDeriveData(SongChange)
|
||||||
|
}
|
||||||
483
tracker/instrument.go
Normal file
483
tracker/instrument.go
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
|
"github.com/vsariola/sointu/vm"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Instrument returns the Instrument view of the model, containing methods to
|
||||||
|
// manipulate the instruments.
|
||||||
|
func (m *Model) Instrument() *InstrModel { return (*InstrModel)(m) }
|
||||||
|
|
||||||
|
type InstrModel Model
|
||||||
|
|
||||||
|
// Add returns an Action to add a new instrument.
|
||||||
|
func (m *InstrModel) Add() Action { return MakeAction((*addInstrument)(m)) }
|
||||||
|
|
||||||
|
type addInstrument InstrModel
|
||||||
|
|
||||||
|
func (m *addInstrument) Enabled() bool { return (*Model)(m).d.Song.Patch.NumVoices() < vm.MAX_VOICES }
|
||||||
|
func (m *addInstrument) Do() {
|
||||||
|
defer (*Model)(m).change("AddInstrument", SongChange, MajorChange)()
|
||||||
|
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
|
||||||
|
p := sointu.Patch{defaultInstrument.Copy()}
|
||||||
|
t := []sointu.Track{{NumVoices: 1}}
|
||||||
|
_, _, ok := (*Model)(m).addVoices(voiceIndex, p, t, true, (*Model)(m).linkInstrTrack)
|
||||||
|
m.changeCancel = !ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete returns an Action to delete the currently selected instrument(s).
|
||||||
|
func (m *InstrModel) Delete() Action { return MakeAction((*deleteInstrument)(m)) }
|
||||||
|
|
||||||
|
type deleteInstrument InstrModel
|
||||||
|
|
||||||
|
func (m *deleteInstrument) Enabled() bool { return len((*Model)(m).d.Song.Patch) > 0 }
|
||||||
|
func (m *deleteInstrument) Do() { (*Model)(m).Instrument().List().DeleteElements(false) }
|
||||||
|
|
||||||
|
// Split returns an Action to split the currently selected instrument, dividing
|
||||||
|
// the voices as evenly as possible.
|
||||||
|
func (m *InstrModel) Split() Action { return MakeAction((*splitInstrument)(m)) }
|
||||||
|
|
||||||
|
type splitInstrument InstrModel
|
||||||
|
|
||||||
|
func (m *splitInstrument) Enabled() bool {
|
||||||
|
return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch) && m.d.Song.Patch[m.d.InstrIndex].NumVoices > 1
|
||||||
|
}
|
||||||
|
func (m *splitInstrument) Do() {
|
||||||
|
defer (*Model)(m).change("SplitInstrument", SongChange, MajorChange)()
|
||||||
|
voiceIndex := m.d.Song.Patch.Copy().FirstVoiceForInstrument(m.d.InstrIndex)
|
||||||
|
middle := voiceIndex + (m.d.Song.Patch[m.d.InstrIndex].NumVoices+1)/2
|
||||||
|
end := voiceIndex + m.d.Song.Patch[m.d.InstrIndex].NumVoices
|
||||||
|
left, ok := VoiceSlice(m.d.Song.Patch, Range{math.MinInt, middle})
|
||||||
|
if !ok {
|
||||||
|
m.changeCancel = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
right, ok := VoiceSlice(m.d.Song.Patch, Range{end, math.MaxInt})
|
||||||
|
if !ok {
|
||||||
|
m.changeCancel = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newInstrument := defaultInstrument.Copy()
|
||||||
|
(*Model)(m).assignUnitIDs(newInstrument.Units)
|
||||||
|
newInstrument.NumVoices = end - middle
|
||||||
|
m.d.Song.Patch = append(left, newInstrument)
|
||||||
|
m.d.Song.Patch = append(m.d.Song.Patch, right...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item returns information about the instrument at a given index.
|
||||||
|
func (v *InstrModel) Item(i int) (name string, maxLevel float32, mute bool, ok bool) {
|
||||||
|
if i < 0 || i >= len(v.d.Song.Patch) {
|
||||||
|
return "", 0, false, false
|
||||||
|
}
|
||||||
|
name = v.d.Song.Patch[i].Name
|
||||||
|
mute = v.d.Song.Patch[i].Mute
|
||||||
|
start := v.d.Song.Patch.FirstVoiceForInstrument(i)
|
||||||
|
end := start + v.d.Song.Patch[i].NumVoices
|
||||||
|
if end >= vm.MAX_VOICES {
|
||||||
|
end = vm.MAX_VOICES
|
||||||
|
}
|
||||||
|
if start < end {
|
||||||
|
for _, level := range v.playerStatus.VoiceLevels[start:end] {
|
||||||
|
if maxLevel < level {
|
||||||
|
maxLevel = level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab returns an Int representing the currently selected instrument tab.
|
||||||
|
func (m *InstrModel) Tab() Int { return MakeInt((*instrumentTab)(m)) }
|
||||||
|
|
||||||
|
type instrumentTab InstrModel
|
||||||
|
|
||||||
|
func (v *instrumentTab) Value() int { return int(v.d.InstrumentTab) }
|
||||||
|
func (v *instrumentTab) Range() RangeInclusive { return RangeInclusive{0, int(NumInstrumentTabs) - 1} }
|
||||||
|
func (v *instrumentTab) SetValue(value int) bool {
|
||||||
|
v.d.InstrumentTab = InstrumentTab(value)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns a List of all the instruments in the patch, implementing
|
||||||
|
// ListData and MutableListData interfaces.
|
||||||
|
func (m *InstrModel) List() List { return List{(*instrumentList)(m)} }
|
||||||
|
|
||||||
|
type instrumentList InstrModel
|
||||||
|
|
||||||
|
func (v *instrumentList) Count() int { return len(v.d.Song.Patch) }
|
||||||
|
func (v *instrumentList) Selected() int { return v.d.InstrIndex }
|
||||||
|
func (v *instrumentList) Selected2() int { return v.d.InstrIndex2 }
|
||||||
|
func (v *instrumentList) SetSelected2(value int) { v.d.InstrIndex2 = value }
|
||||||
|
func (v *instrumentList) SetSelected(value int) {
|
||||||
|
v.d.InstrIndex = value
|
||||||
|
v.d.UnitIndex = 0
|
||||||
|
v.d.UnitIndex2 = 0
|
||||||
|
v.d.UnitSearching = false
|
||||||
|
v.d.UnitSearchString = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *instrumentList) Move(r Range, delta int) (ok bool) {
|
||||||
|
voiceDelta := 0
|
||||||
|
if delta < 0 {
|
||||||
|
voiceDelta = -VoiceRange(v.d.Song.Patch, Range{r.Start + delta, r.Start}).Len()
|
||||||
|
} else if delta > 0 {
|
||||||
|
voiceDelta = VoiceRange(v.d.Song.Patch, Range{r.End, r.End + delta}).Len()
|
||||||
|
}
|
||||||
|
if voiceDelta == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ranges := MakeMoveRanges(VoiceRange(v.d.Song.Patch, r), voiceDelta)
|
||||||
|
return (*Model)(v).sliceInstrumentsTracks(true, v.linkInstrTrack, ranges[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *instrumentList) Delete(r Range) (ok bool) {
|
||||||
|
ranges := Complement(VoiceRange(v.d.Song.Patch, r))
|
||||||
|
return (*Model)(v).sliceInstrumentsTracks(true, v.linkInstrTrack, ranges[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *instrumentList) Change(n string, severity ChangeSeverity) func() {
|
||||||
|
return (*Model)(v).change("Instruments."+n, SongChange, severity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *instrumentList) Cancel() {
|
||||||
|
v.changeCancel = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *instrumentList) Marshal(r Range) ([]byte, error) {
|
||||||
|
return (*Model)(v).marshalVoices(VoiceRange(v.d.Song.Patch, r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *instrumentList) Unmarshal(data []byte) (r Range, err error) {
|
||||||
|
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
|
||||||
|
r, _, ok := (*Model)(m).unmarshalVoices(voiceIndex, data, true, m.linkInstrTrack)
|
||||||
|
if !ok {
|
||||||
|
return Range{}, fmt.Errorf("unmarshal: unmarshalVoices failed")
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread methods
|
||||||
|
type (
|
||||||
|
instrumentThread1 Model
|
||||||
|
instrumentThread2 Model
|
||||||
|
instrumentThread3 Model
|
||||||
|
instrumentThread4 Model
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *InstrModel) Thread1() Bool { return MakeBool((*instrumentThread1)(m)) }
|
||||||
|
func (m *instrumentThread1) Value() bool { return (*InstrModel)(m).getThreadsBit(0) }
|
||||||
|
func (m *instrumentThread1) SetValue(val bool) { (*InstrModel)(m).setThreadsBit(0, val) }
|
||||||
|
func (m *InstrModel) Thread2() Bool { return MakeBool((*instrumentThread2)(m)) }
|
||||||
|
func (m *instrumentThread2) Value() bool { return (*InstrModel)(m).getThreadsBit(1) }
|
||||||
|
func (m *instrumentThread2) SetValue(val bool) { (*InstrModel)(m).setThreadsBit(1, val) }
|
||||||
|
func (m *InstrModel) Thread3() Bool { return MakeBool((*instrumentThread3)(m)) }
|
||||||
|
func (m *instrumentThread3) Value() bool { return (*InstrModel)(m).getThreadsBit(2) }
|
||||||
|
func (m *instrumentThread3) SetValue(val bool) { (*InstrModel)(m).setThreadsBit(2, val) }
|
||||||
|
func (m *InstrModel) Thread4() Bool { return MakeBool((*instrumentThread4)(m)) }
|
||||||
|
func (m *instrumentThread4) Value() bool { return (*InstrModel)(m).getThreadsBit(3) }
|
||||||
|
func (m *instrumentThread4) SetValue(val bool) { (*InstrModel)(m).setThreadsBit(3, val) }
|
||||||
|
|
||||||
|
func (m *InstrModel) getThreadsBit(bit int) bool {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mask := m.d.Song.Patch[m.d.InstrIndex].ThreadMaskM1 + 1
|
||||||
|
return mask&(1<<bit) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InstrModel) setThreadsBit(bit int, value bool) {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mask := m.d.Song.Patch[m.d.InstrIndex].ThreadMaskM1 + 1
|
||||||
|
if value {
|
||||||
|
mask |= (1 << bit)
|
||||||
|
} else {
|
||||||
|
mask &^= (1 << bit)
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("ThreadBitMask", PatchChange, MinorChange)()
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].ThreadMaskM1 = max(mask-1, -1) // -1 has all threads disabled, we warn about that
|
||||||
|
m.warnAboutCrossThreadSends()
|
||||||
|
m.warnNoMultithreadSupport()
|
||||||
|
m.warnNoThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InstrModel) warnAboutCrossThreadSends() {
|
||||||
|
for i, instr := range m.d.Song.Patch {
|
||||||
|
for _, unit := range instr.Units {
|
||||||
|
if unit.Type == "send" {
|
||||||
|
targetID, ok := unit.Parameters["target"]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
it, _, err := m.d.Song.Patch.FindUnit(targetID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if instr.ThreadMaskM1 != m.d.Song.Patch[it].ThreadMaskM1 {
|
||||||
|
(*Alerts)(m).AddNamed("CrossThreadSend", fmt.Sprintf("Instrument %d '%s' has a send to instrument %d '%s' but they are not on the same threads, which may cause issues", i+1, instr.Name, it+1, m.d.Song.Patch[it].Name), Warning)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*Alerts)(m).ClearNamed("CrossThreadSend")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InstrModel) warnNoMultithreadSupport() {
|
||||||
|
for _, instr := range m.d.Song.Patch {
|
||||||
|
if instr.ThreadMaskM1 > 0 && !m.synthers[m.syntherIndex].SupportsMultithreading() {
|
||||||
|
(*Alerts)(m).AddNamed("NoMultithreadSupport", "The current synth does not support multithreading and the patch was configured to use more than one thread", Warning)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*Alerts)(m).ClearNamed("NoMultithreadSupport")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InstrModel) warnNoThread() {
|
||||||
|
for i, instr := range m.d.Song.Patch {
|
||||||
|
if instr.ThreadMaskM1 == -1 {
|
||||||
|
(*Alerts)(m).AddNamed("NoThread", fmt.Sprintf("Instrument %d '%s' is not rendered on any thread", i+1, instr.Name), Warning)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*Alerts)(m).ClearNamed("NoThread")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mute returns a Bool for muting/unmuting the currently selected instrument(s).
|
||||||
|
func (m *InstrModel) Mute() Bool { return MakeBool((*muteInstrument)(m)) }
|
||||||
|
|
||||||
|
type muteInstrument Model
|
||||||
|
|
||||||
|
func (m *muteInstrument) Value() bool {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.d.Song.Patch[m.d.InstrIndex].Mute
|
||||||
|
}
|
||||||
|
func (m *muteInstrument) SetValue(val bool) {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("Mute", PatchChange, MinorChange)()
|
||||||
|
a, b := min(m.d.InstrIndex, m.d.InstrIndex2), max(m.d.InstrIndex, m.d.InstrIndex2)
|
||||||
|
for i := a; i <= b; i++ {
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.d.Song.Patch[i].Mute = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (m *muteInstrument) Enabled() bool {
|
||||||
|
return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo returns a Bool for soloing/unsoloing the currently selected instrument(s).
|
||||||
|
func (m *InstrModel) Solo() Bool { return MakeBool((*soloInstrument)(m)) }
|
||||||
|
|
||||||
|
type soloInstrument Model
|
||||||
|
|
||||||
|
func (m *soloInstrument) Value() bool {
|
||||||
|
a, b := min(m.d.InstrIndex, m.d.InstrIndex2), max(m.d.InstrIndex, m.d.InstrIndex2)
|
||||||
|
for i := range m.d.Song.Patch {
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (i >= a && i <= b) == m.d.Song.Patch[i].Mute {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (m *soloInstrument) SetValue(val bool) {
|
||||||
|
defer (*Model)(m).change("Solo", PatchChange, MinorChange)()
|
||||||
|
a, b := min(m.d.InstrIndex, m.d.InstrIndex2), max(m.d.InstrIndex, m.d.InstrIndex2)
|
||||||
|
for i := range m.d.Song.Patch {
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.d.Song.Patch[i].Mute = !(i >= a && i <= b) && val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (m *soloInstrument) Enabled() bool {
|
||||||
|
return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns a String representing the name of the currently selected
|
||||||
|
// instrument.
|
||||||
|
func (m *InstrModel) Name() String { return MakeString((*instrumentName)(m)) }
|
||||||
|
|
||||||
|
type instrumentName InstrModel
|
||||||
|
|
||||||
|
func (v *instrumentName) Value() string {
|
||||||
|
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return v.d.Song.Patch[v.d.InstrIndex].Name
|
||||||
|
}
|
||||||
|
func (v *instrumentName) SetValue(value string) bool {
|
||||||
|
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer (*Model)(v).change("InstrumentNameString", PatchChange, MinorChange)()
|
||||||
|
v.d.Song.Patch[v.d.InstrIndex].Name = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment returns a String representing the comment of the currently selected
|
||||||
|
// instrument.
|
||||||
|
func (m *InstrModel) Comment() String { return MakeString((*instrumentComment)(m)) }
|
||||||
|
|
||||||
|
type instrumentComment InstrModel
|
||||||
|
|
||||||
|
func (v *instrumentComment) Value() string {
|
||||||
|
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return v.d.Song.Patch[v.d.InstrIndex].Comment
|
||||||
|
}
|
||||||
|
func (v *instrumentComment) SetValue(value string) bool {
|
||||||
|
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer (*Model)(v).change("InstrumentComment", PatchChange, MinorChange)()
|
||||||
|
v.d.Song.Patch[v.d.InstrIndex].Comment = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Voices returns an Int representing the number of voices for the currently
|
||||||
|
// selected instrument.
|
||||||
|
func (m *InstrModel) Voices() Int { return MakeInt((*instrumentVoices)(m)) }
|
||||||
|
|
||||||
|
type instrumentVoices InstrModel
|
||||||
|
|
||||||
|
func (v *instrumentVoices) Value() int {
|
||||||
|
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return max(v.d.Song.Patch[v.d.InstrIndex].NumVoices, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *instrumentVoices) SetValue(value int) bool {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("InstrumentVoices", SongChange, MinorChange)()
|
||||||
|
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
|
||||||
|
voiceRange := Range{voiceIndex, voiceIndex + m.d.Song.Patch[m.d.InstrIndex].NumVoices}
|
||||||
|
ranges := MakeSetLength(voiceRange, value)
|
||||||
|
ok := (*Model)(m).sliceInstrumentsTracks(true, m.linkInstrTrack, ranges...)
|
||||||
|
if !ok {
|
||||||
|
m.changeCancel = true
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *instrumentVoices) Range() RangeInclusive {
|
||||||
|
return RangeInclusive{1, (*Model)(v).remainingVoices(true, v.linkInstrTrack) + v.Value()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes the currently selected instrument to the given io.WriteCloser.
|
||||||
|
// If the WriteCloser is a file, the file extension is used to determine the
|
||||||
|
// format (.json for JSON, anything else for YAML).
|
||||||
|
func (m *InstrModel) Write(w io.WriteCloser) bool {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
(*Model)(m).Alerts().Add("No instrument selected", Error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
path := ""
|
||||||
|
if f, ok := w.(*os.File); ok {
|
||||||
|
path = f.Name()
|
||||||
|
}
|
||||||
|
var extension = filepath.Ext(path)
|
||||||
|
var contents []byte
|
||||||
|
var err error
|
||||||
|
instr := m.d.Song.Patch[m.d.InstrIndex]
|
||||||
|
if _, ok := w.(*os.File); ok {
|
||||||
|
instr.Name = "" // don't save the instrument name to a file; we'll replace the instruments name with the filename when loading from a file
|
||||||
|
}
|
||||||
|
if extension == ".json" {
|
||||||
|
contents, err = json.Marshal(instr)
|
||||||
|
} else {
|
||||||
|
contents, err = yaml.Marshal(instr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
(*Model)(m).Alerts().Add(fmt.Sprintf("Error marshaling an instrument file: %v", err), Error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
w.Write(contents)
|
||||||
|
w.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads an instrument from the given io.ReadCloser and sets it as the
|
||||||
|
// currently selected instrument. The format is determined by trying JSON first, then
|
||||||
|
// YAML, then 4klang Patch, then 4klang Instrument.
|
||||||
|
func (m *InstrModel) Read(r io.ReadCloser) bool {
|
||||||
|
if m.d.InstrIndex < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
b, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
r.Close() // if we can't close the file, it's not a big deal, so ignore the error
|
||||||
|
var instrument sointu.Instrument
|
||||||
|
var errJSON, errYaml, err4ki, err4kp error
|
||||||
|
var patch sointu.Patch
|
||||||
|
errJSON = json.Unmarshal(b, &instrument)
|
||||||
|
if errJSON == nil {
|
||||||
|
goto success
|
||||||
|
}
|
||||||
|
errYaml = yaml.Unmarshal(b, &instrument)
|
||||||
|
if errYaml == nil {
|
||||||
|
goto success
|
||||||
|
}
|
||||||
|
patch, err4kp = sointu.Read4klangPatch(bytes.NewReader(b))
|
||||||
|
if err4kp == nil {
|
||||||
|
defer (*Model)(m).change("LoadInstrument", PatchChange, MajorChange)()
|
||||||
|
m.d.Song.Patch = patch
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
instrument, err4ki = sointu.Read4klangInstrument(bytes.NewReader(b))
|
||||||
|
if err4ki == nil {
|
||||||
|
goto success
|
||||||
|
}
|
||||||
|
(*Model)(m).Alerts().Add(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v / %v / %v", errYaml, errJSON, err4ki, err4kp), Error)
|
||||||
|
return false
|
||||||
|
success:
|
||||||
|
if f, ok := r.(*os.File); ok {
|
||||||
|
filename := f.Name()
|
||||||
|
// the instrument names are generally junk, replace them with the filename without extension
|
||||||
|
instrument.Name = filepath.Base(filename[:len(filename)-len(filepath.Ext(filename))])
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("LoadInstrument", PatchChange, MajorChange)()
|
||||||
|
for len(m.d.Song.Patch) <= m.d.InstrIndex {
|
||||||
|
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
|
||||||
|
}
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex] = sointu.Instrument{}
|
||||||
|
numVoices := m.d.Song.Patch.NumVoices()
|
||||||
|
if numVoices >= vm.MAX_VOICES {
|
||||||
|
// this really shouldn't happen, as we have already cleared the
|
||||||
|
// instrument and assuming each instrument has at least 1 voice, it
|
||||||
|
// should have freed up some voices
|
||||||
|
(*Model)(m).Alerts().Add(fmt.Sprintf("The patch has already %d voices", vm.MAX_VOICES), Error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
instrument.NumVoices = clamp(instrument.NumVoices, 1, 32-numVoices)
|
||||||
|
(*Model)(m).assignUnitIDs(instrument.Units)
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex] = instrument
|
||||||
|
return true
|
||||||
|
}
|
||||||
255
tracker/int.go
255
tracker/int.go
@@ -1,255 +0,0 @@
|
|||||||
package tracker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// Int represents an integer value in the tracker model e.g. BPM, song
|
|
||||||
// length, etc. It is a wrapper around an IntValue interface that provides
|
|
||||||
// methods to manipulate the value, but Int guard that all changes are
|
|
||||||
// within the range of the underlying IntValue implementation and that
|
|
||||||
// SetValue is not called when the value is unchanged.
|
|
||||||
Int struct {
|
|
||||||
value IntValue
|
|
||||||
}
|
|
||||||
|
|
||||||
IntValue interface {
|
|
||||||
Value() int
|
|
||||||
SetValue(int) bool // returns true if the value was changed
|
|
||||||
Range() IntRange
|
|
||||||
}
|
|
||||||
|
|
||||||
IntRange struct {
|
|
||||||
Min, Max int
|
|
||||||
}
|
|
||||||
|
|
||||||
InstrumentVoices Model
|
|
||||||
TrackVoices Model
|
|
||||||
SongLength Model
|
|
||||||
BPM Model
|
|
||||||
RowsPerPattern Model
|
|
||||||
RowsPerBeat Model
|
|
||||||
Step Model
|
|
||||||
Octave Model
|
|
||||||
DetectorWeighting Model
|
|
||||||
SyntherIndex Model
|
|
||||||
SpecAnSpeed Model
|
|
||||||
SpecAnResolution Model
|
|
||||||
SpecAnChannelsInt Model
|
|
||||||
)
|
|
||||||
|
|
||||||
func MakeInt(value IntValue) Int {
|
|
||||||
return Int{value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Int) Add(delta int) (ok bool) {
|
|
||||||
return v.SetValue(v.Value() + delta)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Int) SetValue(value int) (ok bool) {
|
|
||||||
r := v.Range()
|
|
||||||
value = r.Clamp(value)
|
|
||||||
if value == v.Value() || value < r.Min || value > r.Max {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return v.value.SetValue(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Int) Range() IntRange {
|
|
||||||
if v.value == nil {
|
|
||||||
return IntRange{0, 0}
|
|
||||||
}
|
|
||||||
return v.value.Range()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Int) Value() int {
|
|
||||||
if v.value == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return v.value.Value()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r IntRange) Clamp(value int) int {
|
|
||||||
return max(min(value, r.Max), r.Min)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Model methods
|
|
||||||
|
|
||||||
func (m *Model) BPM() Int { return MakeInt((*BPM)(m)) }
|
|
||||||
func (m *Model) InstrumentVoices() Int { return MakeInt((*InstrumentVoices)(m)) }
|
|
||||||
func (m *Model) TrackVoices() Int { return MakeInt((*TrackVoices)(m)) }
|
|
||||||
func (m *Model) SongLength() Int { return MakeInt((*SongLength)(m)) }
|
|
||||||
func (m *Model) RowsPerPattern() Int { return MakeInt((*RowsPerPattern)(m)) }
|
|
||||||
func (m *Model) RowsPerBeat() Int { return MakeInt((*RowsPerBeat)(m)) }
|
|
||||||
func (m *Model) Step() Int { return MakeInt((*Step)(m)) }
|
|
||||||
func (m *Model) Octave() Int { return MakeInt((*Octave)(m)) }
|
|
||||||
func (m *Model) DetectorWeighting() Int { return MakeInt((*DetectorWeighting)(m)) }
|
|
||||||
func (m *Model) SyntherIndex() Int { return MakeInt((*SyntherIndex)(m)) }
|
|
||||||
func (m *Model) SpecAnSpeed() Int { return MakeInt((*SpecAnSpeed)(m)) }
|
|
||||||
func (m *Model) SpecAnResolution() Int { return MakeInt((*SpecAnResolution)(m)) }
|
|
||||||
func (m *Model) SpecAnChannelsInt() Int { return MakeInt((*SpecAnChannelsInt)(m)) }
|
|
||||||
|
|
||||||
// BeatsPerMinuteInt
|
|
||||||
|
|
||||||
func (v *BPM) Value() int { return v.d.Song.BPM }
|
|
||||||
func (v *BPM) SetValue(value int) bool {
|
|
||||||
defer (*Model)(v).change("BPMInt", SongChange, MinorChange)()
|
|
||||||
v.d.Song.BPM = value
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
func (v *BPM) Range() IntRange { return IntRange{1, 999} }
|
|
||||||
|
|
||||||
// RowsPerPatternInt
|
|
||||||
|
|
||||||
func (v *RowsPerPattern) Value() int { return v.d.Song.Score.RowsPerPattern }
|
|
||||||
func (v *RowsPerPattern) SetValue(value int) bool {
|
|
||||||
defer (*Model)(v).change("RowsPerPatternInt", SongChange, MinorChange)()
|
|
||||||
v.d.Song.Score.RowsPerPattern = value
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
func (v *RowsPerPattern) Range() IntRange { return IntRange{1, 256} }
|
|
||||||
|
|
||||||
// SongLengthInt
|
|
||||||
|
|
||||||
func (v *SongLength) Value() int { return v.d.Song.Score.Length }
|
|
||||||
func (v *SongLength) SetValue(value int) bool {
|
|
||||||
defer (*Model)(v).change("SongLengthInt", SongChange, MinorChange)()
|
|
||||||
v.d.Song.Score.Length = value
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
func (v *SongLength) Range() IntRange { return IntRange{1, math.MaxInt32} }
|
|
||||||
|
|
||||||
// StepInt
|
|
||||||
|
|
||||||
func (v *Step) Value() int { return v.d.Step }
|
|
||||||
func (v *Step) SetValue(value int) bool {
|
|
||||||
defer (*Model)(v).change("StepInt", NoChange, MinorChange)()
|
|
||||||
v.d.Step = value
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
func (v *Step) Range() IntRange { return IntRange{0, 8} }
|
|
||||||
|
|
||||||
// OctaveInt
|
|
||||||
|
|
||||||
func (v *Octave) Value() int { return v.d.Octave }
|
|
||||||
func (v *Octave) SetValue(value int) bool { v.d.Octave = value; return true }
|
|
||||||
func (v *Octave) Range() IntRange { return IntRange{0, 9} }
|
|
||||||
|
|
||||||
// RowsPerBeatInt
|
|
||||||
|
|
||||||
func (v *RowsPerBeat) Value() int { return v.d.Song.RowsPerBeat }
|
|
||||||
func (v *RowsPerBeat) SetValue(value int) bool {
|
|
||||||
defer (*Model)(v).change("RowsPerBeatInt", SongChange, MinorChange)()
|
|
||||||
v.d.Song.RowsPerBeat = value
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
func (v *RowsPerBeat) Range() IntRange { return IntRange{1, 32} }
|
|
||||||
|
|
||||||
// ModelLoudnessType
|
|
||||||
|
|
||||||
func (v *DetectorWeighting) Value() int { return int(v.weightingType) }
|
|
||||||
func (v *DetectorWeighting) SetValue(value int) bool {
|
|
||||||
v.weightingType = WeightingType(value)
|
|
||||||
TrySend(v.broker.ToDetector, MsgToDetector{HasWeightingType: true, WeightingType: WeightingType(value)})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
func (v *DetectorWeighting) Range() IntRange { return IntRange{0, int(NumLoudnessTypes) - 1} }
|
|
||||||
|
|
||||||
// SpecAn stuff
|
|
||||||
|
|
||||||
func (v *SpecAnSpeed) Value() int { return int(v.specAnSettings.Smooth) }
|
|
||||||
func (v *SpecAnSpeed) SetValue(value int) bool {
|
|
||||||
v.specAnSettings.Smooth = value
|
|
||||||
TrySend(v.broker.ToSpecAn, MsgToSpecAn{HasSettings: true, SpecSettings: v.specAnSettings})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
func (v *SpecAnSpeed) Range() IntRange { return IntRange{SpecSpeedMin, SpecSpeedMax} }
|
|
||||||
|
|
||||||
func (v *SpecAnResolution) Value() int { return v.specAnSettings.Resolution }
|
|
||||||
func (v *SpecAnResolution) SetValue(value int) bool {
|
|
||||||
v.specAnSettings.Resolution = value
|
|
||||||
TrySend(v.broker.ToSpecAn, MsgToSpecAn{HasSettings: true, SpecSettings: v.specAnSettings})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
func (v *SpecAnResolution) Range() IntRange { return IntRange{SpecResolutionMin, SpecResolutionMax} }
|
|
||||||
|
|
||||||
func (v *SpecAnChannelsInt) Value() int { return int(v.specAnSettings.ChnMode) }
|
|
||||||
func (v *SpecAnChannelsInt) SetValue(value int) bool {
|
|
||||||
v.specAnSettings.ChnMode = SpecChnMode(value)
|
|
||||||
TrySend(v.broker.ToSpecAn, MsgToSpecAn{HasSettings: true, SpecSettings: v.specAnSettings})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
func (v *SpecAnChannelsInt) Range() IntRange { return IntRange{0, int(NumSpecChnModes) - 1} }
|
|
||||||
|
|
||||||
// InstrumentVoicesInt
|
|
||||||
|
|
||||||
func (v *InstrumentVoices) Value() int {
|
|
||||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return max(v.d.Song.Patch[v.d.InstrIndex].NumVoices, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *InstrumentVoices) SetValue(value int) bool {
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer (*Model)(m).change("InstrumentVoices", SongChange, MinorChange)()
|
|
||||||
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
|
|
||||||
voiceRange := Range{voiceIndex, voiceIndex + m.d.Song.Patch[m.d.InstrIndex].NumVoices}
|
|
||||||
ranges := MakeSetLength(voiceRange, value)
|
|
||||||
ok := (*Model)(m).sliceInstrumentsTracks(true, m.linkInstrTrack, ranges...)
|
|
||||||
if !ok {
|
|
||||||
m.changeCancel = true
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *InstrumentVoices) Range() IntRange {
|
|
||||||
return IntRange{1, (*Model)(v).remainingVoices(true, v.linkInstrTrack) + v.Value()}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrackVoicesInt
|
|
||||||
|
|
||||||
func (v *TrackVoices) Value() int {
|
|
||||||
t := v.d.Cursor.Track
|
|
||||||
if t < 0 || t >= len(v.d.Song.Score.Tracks) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return max(v.d.Song.Score.Tracks[t].NumVoices, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *TrackVoices) SetValue(value int) bool {
|
|
||||||
defer (*Model)(m).change("TrackVoices", SongChange, MinorChange)()
|
|
||||||
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
|
|
||||||
voiceRange := Range{voiceIndex, voiceIndex + m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices}
|
|
||||||
ranges := MakeSetLength(voiceRange, value)
|
|
||||||
ok := (*Model)(m).sliceInstrumentsTracks(m.linkInstrTrack, true, ranges...)
|
|
||||||
if !ok {
|
|
||||||
m.changeCancel = true
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *TrackVoices) Range() IntRange {
|
|
||||||
t := v.d.Cursor.Track
|
|
||||||
if t < 0 || t >= len(v.d.Song.Score.Tracks) {
|
|
||||||
return IntRange{1, 1}
|
|
||||||
}
|
|
||||||
return IntRange{1, (*Model)(v).remainingVoices(v.linkInstrTrack, true) + v.d.Song.Score.Tracks[t].NumVoices}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyntherIndex
|
|
||||||
|
|
||||||
func (v *SyntherIndex) Value() int { return v.syntherIndex }
|
|
||||||
func (v *SyntherIndex) Range() IntRange { return IntRange{0, len(v.synthers) - 1} }
|
|
||||||
func (v *Model) SyntherName() string { return v.synthers[v.syntherIndex].Name() }
|
|
||||||
func (v *SyntherIndex) SetValue(value int) bool {
|
|
||||||
if value < 0 || value >= len(v.synthers) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
v.syntherIndex = value
|
|
||||||
TrySend(v.broker.ToPlayer, any(v.synthers[value]))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
890
tracker/list.go
890
tracker/list.go
@@ -1,890 +0,0 @@
|
|||||||
package tracker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"iter"
|
|
||||||
"math"
|
|
||||||
"math/bits"
|
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
|
||||||
"github.com/vsariola/sointu/vm"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
List struct {
|
|
||||||
data ListData
|
|
||||||
}
|
|
||||||
|
|
||||||
ListData interface {
|
|
||||||
Selected() int
|
|
||||||
Selected2() int
|
|
||||||
SetSelected(int)
|
|
||||||
SetSelected2(int)
|
|
||||||
Count() int
|
|
||||||
}
|
|
||||||
|
|
||||||
MutableListData interface {
|
|
||||||
Change(kind string, severity ChangeSeverity) func()
|
|
||||||
Cancel()
|
|
||||||
Move(r Range, delta int) (ok bool)
|
|
||||||
Delete(r Range) (ok bool)
|
|
||||||
Marshal(r Range) ([]byte, error)
|
|
||||||
Unmarshal([]byte) (r Range, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Range is used to represent a range [Start,End) of integers
|
|
||||||
Range struct {
|
|
||||||
Start, End int
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func MakeList(data ListData) List { return List{data} }
|
|
||||||
|
|
||||||
func (l List) Selected() int { return max(min(l.data.Selected(), l.data.Count()-1), 0) }
|
|
||||||
func (l List) Selected2() int { return max(min(l.data.Selected2(), l.data.Count()-1), 0) }
|
|
||||||
func (l List) SetSelected(value int) { l.data.SetSelected(max(min(value, l.data.Count()-1), 0)) }
|
|
||||||
func (l List) SetSelected2(value int) { l.data.SetSelected2(max(min(value, l.data.Count()-1), 0)) }
|
|
||||||
func (l List) Count() int { return l.data.Count() }
|
|
||||||
|
|
||||||
// MoveElements moves the selected elements in a list by delta. The list must
|
|
||||||
// implement the MutableListData interface.
|
|
||||||
func (v List) MoveElements(delta int) bool {
|
|
||||||
s, ok := v.data.(MutableListData)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
r := v.listRange()
|
|
||||||
if delta == 0 || r.Start+delta < 0 || r.End+delta > v.Count() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer s.Change("MoveElements", MajorChange)()
|
|
||||||
if !s.Move(r, delta) {
|
|
||||||
s.Cancel()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
v.SetSelected(v.Selected() + delta)
|
|
||||||
v.SetSelected2(v.Selected2() + delta)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteElements deletes the selected elements in a list. The list must
|
|
||||||
// implement the MutableListData interface.
|
|
||||||
func (v List) DeleteElements(backwards bool) bool {
|
|
||||||
d, ok := v.data.(MutableListData)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
r := v.listRange()
|
|
||||||
if r.Len() == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer d.Change("DeleteElements", MajorChange)()
|
|
||||||
if !d.Delete(r) {
|
|
||||||
d.Cancel()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if backwards && r.Start > 0 {
|
|
||||||
r.Start--
|
|
||||||
}
|
|
||||||
v.SetSelected(r.Start)
|
|
||||||
v.SetSelected2(r.Start)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyElements copies the selected elements in a list. The list must implement
|
|
||||||
// the MutableListData interface. Returns the copied data, marshaled into byte
|
|
||||||
// slice, and true if successful.
|
|
||||||
func (v List) CopyElements() ([]byte, bool) {
|
|
||||||
m, ok := v.data.(MutableListData)
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
r := v.listRange()
|
|
||||||
if r.Len() == 0 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
ret, err := m.Marshal(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return ret, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// PasteElements pastes the data into the list. The data is unmarshaled from the
|
|
||||||
// byte slice. The list must implement the MutableListData interface. Returns
|
|
||||||
// true if successful.
|
|
||||||
func (v List) PasteElements(data []byte) (ok bool) {
|
|
||||||
m, ok := v.data.(MutableListData)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer m.Change("PasteElements", MajorChange)()
|
|
||||||
r, err := m.Unmarshal(data)
|
|
||||||
if err != nil {
|
|
||||||
m.Cancel()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
v.SetSelected(r.Start)
|
|
||||||
v.SetSelected2(r.End - 1)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v List) Mutable() bool {
|
|
||||||
_, ok := v.data.(MutableListData)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *List) listRange() (r Range) {
|
|
||||||
r.Start = max(min(v.Selected(), v.Selected2()), 0)
|
|
||||||
r.End = min(max(v.Selected(), v.Selected2())+1, v.Count())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// instruments is a list of instruments, implementing ListData & MutableListData interfaces
|
|
||||||
type instruments Model
|
|
||||||
|
|
||||||
func (m *Model) Instruments() List { return List{(*instruments)(m)} }
|
|
||||||
|
|
||||||
func (v *Model) Instrument(i int) (name string, maxLevel float32, mute bool, ok bool) {
|
|
||||||
if i < 0 || i >= len(v.d.Song.Patch) {
|
|
||||||
return "", 0, false, false
|
|
||||||
}
|
|
||||||
name = v.d.Song.Patch[i].Name
|
|
||||||
mute = v.d.Song.Patch[i].Mute
|
|
||||||
start := v.d.Song.Patch.FirstVoiceForInstrument(i)
|
|
||||||
end := start + v.d.Song.Patch[i].NumVoices
|
|
||||||
if end >= vm.MAX_VOICES {
|
|
||||||
end = vm.MAX_VOICES
|
|
||||||
}
|
|
||||||
if start < end {
|
|
||||||
for _, level := range v.playerStatus.VoiceLevels[start:end] {
|
|
||||||
if maxLevel < level {
|
|
||||||
maxLevel = level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ok = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *instruments) Count() int { return len(v.d.Song.Patch) }
|
|
||||||
func (v *instruments) Selected() int { return v.d.InstrIndex }
|
|
||||||
func (v *instruments) Selected2() int { return v.d.InstrIndex2 }
|
|
||||||
func (v *instruments) SetSelected2(value int) { v.d.InstrIndex2 = value }
|
|
||||||
func (v *instruments) SetSelected(value int) {
|
|
||||||
v.d.InstrIndex = value
|
|
||||||
v.d.UnitIndex = 0
|
|
||||||
v.d.UnitIndex2 = 0
|
|
||||||
v.d.UnitSearching = false
|
|
||||||
v.d.UnitSearchString = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *instruments) Move(r Range, delta int) (ok bool) {
|
|
||||||
voiceDelta := 0
|
|
||||||
if delta < 0 {
|
|
||||||
voiceDelta = -VoiceRange(v.d.Song.Patch, Range{r.Start + delta, r.Start}).Len()
|
|
||||||
} else if delta > 0 {
|
|
||||||
voiceDelta = VoiceRange(v.d.Song.Patch, Range{r.End, r.End + delta}).Len()
|
|
||||||
}
|
|
||||||
if voiceDelta == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ranges := MakeMoveRanges(VoiceRange(v.d.Song.Patch, r), voiceDelta)
|
|
||||||
return (*Model)(v).sliceInstrumentsTracks(true, v.linkInstrTrack, ranges[:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *instruments) Delete(r Range) (ok bool) {
|
|
||||||
ranges := Complement(VoiceRange(v.d.Song.Patch, r))
|
|
||||||
return (*Model)(v).sliceInstrumentsTracks(true, v.linkInstrTrack, ranges[:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *instruments) Change(n string, severity ChangeSeverity) func() {
|
|
||||||
return (*Model)(v).change("Instruments."+n, SongChange, severity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *instruments) Cancel() {
|
|
||||||
v.changeCancel = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *instruments) Marshal(r Range) ([]byte, error) {
|
|
||||||
return (*Model)(v).marshalVoices(VoiceRange(v.d.Song.Patch, r))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *instruments) Unmarshal(data []byte) (r Range, err error) {
|
|
||||||
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
|
|
||||||
r, _, ok := (*Model)(m).unmarshalVoices(voiceIndex, data, true, m.linkInstrTrack)
|
|
||||||
if !ok {
|
|
||||||
return Range{}, fmt.Errorf("unmarshal: unmarshalVoices failed")
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// units is a list of all the units in the selected instrument, implementing ListData & MutableListData interfaces
|
|
||||||
type (
|
|
||||||
units Model
|
|
||||||
UnitListItem struct {
|
|
||||||
Type, Comment string
|
|
||||||
Disabled bool
|
|
||||||
Signals Rail
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *Model) Units() List { return List{(*units)(m)} }
|
|
||||||
|
|
||||||
func (v *Model) Unit(index int) UnitListItem {
|
|
||||||
i := v.d.InstrIndex
|
|
||||||
if i < 0 || i >= len(v.d.Song.Patch) || index < 0 || index >= (*units)(v).Count() {
|
|
||||||
return UnitListItem{}
|
|
||||||
}
|
|
||||||
unit := v.d.Song.Patch[v.d.InstrIndex].Units[index]
|
|
||||||
signals := Rail{}
|
|
||||||
if i >= 0 && i < len(v.derived.patch) && index >= 0 && index < len(v.derived.patch[i].rails) {
|
|
||||||
signals = v.derived.patch[i].rails[index]
|
|
||||||
}
|
|
||||||
return UnitListItem{
|
|
||||||
Type: unit.Type,
|
|
||||||
Comment: unit.Comment,
|
|
||||||
Disabled: unit.Disabled,
|
|
||||||
Signals: signals,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SelectedUnitType() string {
|
|
||||||
if m.d.InstrIndex < 0 ||
|
|
||||||
m.d.InstrIndex >= len(m.d.Song.Patch) ||
|
|
||||||
m.d.UnitIndex < 0 ||
|
|
||||||
m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Type
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SetSelectedUnitType(t string) {
|
|
||||||
if m.d.InstrIndex < 0 ||
|
|
||||||
m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if m.d.UnitIndex < 0 {
|
|
||||||
m.d.UnitIndex = 0
|
|
||||||
}
|
|
||||||
for len(m.d.Song.Patch[m.d.InstrIndex].Units) <= m.d.UnitIndex {
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex].Units = append(m.d.Song.Patch[m.d.InstrIndex].Units, sointu.Unit{})
|
|
||||||
}
|
|
||||||
unit, ok := defaultUnits[t]
|
|
||||||
if !ok { // if the type is invalid, we just set it to empty unit
|
|
||||||
unit = sointu.Unit{Parameters: make(map[string]int)}
|
|
||||||
} else {
|
|
||||||
unit = unit.Copy()
|
|
||||||
}
|
|
||||||
oldUnit := m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex]
|
|
||||||
if oldUnit.Type == unit.Type {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer (*units)(m).Change("SetSelectedType", MajorChange)()
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex] = unit
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].ID = oldUnit.ID // keep the ID of the replaced unit
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *units) Selected() int { return v.d.UnitIndex }
|
|
||||||
func (v *units) Selected2() int { return v.d.UnitIndex2 }
|
|
||||||
func (v *units) SetSelected2(value int) { v.d.UnitIndex2 = value }
|
|
||||||
func (m *units) SetSelected(value int) {
|
|
||||||
m.d.UnitIndex = value
|
|
||||||
m.d.ParamIndex = 0
|
|
||||||
m.d.UnitSearching = false
|
|
||||||
m.d.UnitSearchString = ""
|
|
||||||
}
|
|
||||||
func (v *units) Count() int {
|
|
||||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return len(v.d.Song.Patch[v.d.InstrIndex].Units)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *units) Move(r Range, delta int) (ok bool) {
|
|
||||||
m := (*Model)(v)
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
units := m.d.Song.Patch[m.d.InstrIndex].Units
|
|
||||||
for i, j := range r.Swaps(delta) {
|
|
||||||
units[i], units[j] = units[j], units[i]
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *units) Delete(r Range) (ok bool) {
|
|
||||||
m := (*Model)(v)
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
u := m.d.Song.Patch[m.d.InstrIndex].Units
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex].Units = append(u[:r.Start], u[r.End:]...)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *units) Change(n string, severity ChangeSeverity) func() {
|
|
||||||
return (*Model)(v).change("UnitListView."+n, PatchChange, severity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *units) Cancel() {
|
|
||||||
(*Model)(v).changeCancel = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *units) Marshal(r Range) ([]byte, error) {
|
|
||||||
m := (*Model)(v)
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return nil, errors.New("UnitListView.marshal: no instruments")
|
|
||||||
}
|
|
||||||
units := m.d.Song.Patch[m.d.InstrIndex].Units[r.Start:r.End]
|
|
||||||
ret, err := yaml.Marshal(struct{ Units []sointu.Unit }{units})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("UnitListView.marshal: %v", err)
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *units) Unmarshal(data []byte) (r Range, err error) {
|
|
||||||
m := (*Model)(v)
|
|
||||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
return Range{}, errors.New("UnitListView.unmarshal: no instruments")
|
|
||||||
}
|
|
||||||
var pastedUnits struct{ Units []sointu.Unit }
|
|
||||||
if err := yaml.Unmarshal(data, &pastedUnits); err != nil {
|
|
||||||
return Range{}, fmt.Errorf("UnitListView.unmarshal: %v", err)
|
|
||||||
}
|
|
||||||
if len(pastedUnits.Units) == 0 {
|
|
||||||
return Range{}, errors.New("UnitListView.unmarshal: no units")
|
|
||||||
}
|
|
||||||
m.assignUnitIDs(pastedUnits.Units)
|
|
||||||
sel := v.Selected()
|
|
||||||
var ok bool
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex].Units, ok = Insert(m.d.Song.Patch[m.d.InstrIndex].Units, sel, pastedUnits.Units...)
|
|
||||||
if !ok {
|
|
||||||
return Range{}, errors.New("UnitListView.unmarshal: insert failed")
|
|
||||||
}
|
|
||||||
return Range{sel, sel + len(pastedUnits.Units)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// tracks is a list of all the tracks, implementing ListData & MutableListData interfaces
|
|
||||||
type tracks Model
|
|
||||||
|
|
||||||
func (m *Model) Tracks() List { return List{(*tracks)(m)} }
|
|
||||||
|
|
||||||
func (v *tracks) Selected() int { return v.d.Cursor.Track }
|
|
||||||
func (v *tracks) Selected2() int { return v.d.Cursor2.Track }
|
|
||||||
func (v *tracks) SetSelected(value int) { v.d.Cursor.Track = value }
|
|
||||||
func (v *tracks) SetSelected2(value int) { v.d.Cursor2.Track = value }
|
|
||||||
func (v *tracks) Count() int { return len((*Model)(v).d.Song.Score.Tracks) }
|
|
||||||
|
|
||||||
func (v *tracks) Move(r Range, delta int) (ok bool) {
|
|
||||||
voiceDelta := 0
|
|
||||||
if delta < 0 {
|
|
||||||
voiceDelta = -VoiceRange(v.d.Song.Score.Tracks, Range{r.Start + delta, r.Start}).Len()
|
|
||||||
} else if delta > 0 {
|
|
||||||
voiceDelta = VoiceRange(v.d.Song.Score.Tracks, Range{r.End, r.End + delta}).Len()
|
|
||||||
}
|
|
||||||
if voiceDelta == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ranges := MakeMoveRanges(VoiceRange(v.d.Song.Score.Tracks, r), voiceDelta)
|
|
||||||
return (*Model)(v).sliceInstrumentsTracks(v.linkInstrTrack, true, ranges[:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *tracks) Delete(r Range) (ok bool) {
|
|
||||||
ranges := Complement(VoiceRange(v.d.Song.Score.Tracks, r))
|
|
||||||
return (*Model)(v).sliceInstrumentsTracks(v.linkInstrTrack, true, ranges[:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *tracks) Change(n string, severity ChangeSeverity) func() {
|
|
||||||
return (*Model)(v).change("TrackList."+n, SongChange, severity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *tracks) Cancel() {
|
|
||||||
v.changeCancel = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *tracks) Marshal(r Range) ([]byte, error) {
|
|
||||||
return (*Model)(v).marshalVoices(VoiceRange(v.d.Song.Score.Tracks, r))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *tracks) Unmarshal(data []byte) (r Range, err error) {
|
|
||||||
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
|
|
||||||
_, r, ok := (*Model)(m).unmarshalVoices(voiceIndex, data, m.linkInstrTrack, true)
|
|
||||||
if !ok {
|
|
||||||
return Range{}, fmt.Errorf("unmarshal: unmarshalVoices failed")
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// orderRows is a list of all the order rows, implementing ListData & MutableListData interfaces
|
|
||||||
type orderRows Model
|
|
||||||
|
|
||||||
func (m *Model) OrderRows() List { return List{(*orderRows)(m)} }
|
|
||||||
|
|
||||||
func (v *orderRows) Count() int { return v.d.Song.Score.Length }
|
|
||||||
func (v *orderRows) Selected() int { return v.d.Cursor.OrderRow }
|
|
||||||
func (v *orderRows) Selected2() int { return v.d.Cursor2.OrderRow }
|
|
||||||
func (v *orderRows) SetSelected2(value int) { v.d.Cursor2.OrderRow = value }
|
|
||||||
func (v *orderRows) SetSelected(value int) {
|
|
||||||
if value != v.d.Cursor.OrderRow {
|
|
||||||
v.follow = false
|
|
||||||
}
|
|
||||||
v.d.Cursor.OrderRow = value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *orderRows) Move(r Range, delta int) (ok bool) {
|
|
||||||
swaps := r.Swaps(delta)
|
|
||||||
for i, t := range v.d.Song.Score.Tracks {
|
|
||||||
for a, b := range swaps {
|
|
||||||
ea, eb := t.Order.Get(a), t.Order.Get(b)
|
|
||||||
v.d.Song.Score.Tracks[i].Order.Set(a, eb)
|
|
||||||
v.d.Song.Score.Tracks[i].Order.Set(b, ea)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *orderRows) Delete(r Range) (ok bool) {
|
|
||||||
for i, t := range v.d.Song.Score.Tracks {
|
|
||||||
r2 := r.Intersect(Range{0, len(t.Order)})
|
|
||||||
v.d.Song.Score.Tracks[i].Order = append(t.Order[:r2.Start], t.Order[r2.End:]...)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *orderRows) Change(n string, severity ChangeSeverity) func() {
|
|
||||||
return (*Model)(v).change("OrderRowList."+n, ScoreChange, severity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *orderRows) Cancel() {
|
|
||||||
v.changeCancel = true
|
|
||||||
}
|
|
||||||
|
|
||||||
type marshalOrderRows struct {
|
|
||||||
Columns [][]int `yaml:",flow"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *orderRows) Marshal(r Range) ([]byte, error) {
|
|
||||||
var table marshalOrderRows
|
|
||||||
for i := range v.d.Song.Score.Tracks {
|
|
||||||
table.Columns = append(table.Columns, make([]int, r.Len()))
|
|
||||||
for j := 0; j < r.Len(); j++ {
|
|
||||||
table.Columns[i][j] = v.d.Song.Score.Tracks[i].Order.Get(r.Start + j)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return yaml.Marshal(table)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *orderRows) Unmarshal(data []byte) (r Range, err error) {
|
|
||||||
var table marshalOrderRows
|
|
||||||
err = yaml.Unmarshal(data, &table)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(table.Columns) == 0 {
|
|
||||||
err = errors.New("OrderRowList.unmarshal: no rows")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.Start = v.d.Cursor.OrderRow
|
|
||||||
r.End = v.d.Cursor.OrderRow + len(table.Columns[0])
|
|
||||||
for i := range v.d.Song.Score.Tracks {
|
|
||||||
if i >= len(table.Columns) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
order := &v.d.Song.Score.Tracks[i].Order
|
|
||||||
for j := 0; j < r.Start-len(*order); j++ {
|
|
||||||
*order = append(*order, -1)
|
|
||||||
}
|
|
||||||
if len(*order) > r.Start {
|
|
||||||
table.Columns[i] = append(table.Columns[i], (*order)[r.Start:]...)
|
|
||||||
*order = (*order)[:r.Start]
|
|
||||||
}
|
|
||||||
*order = append(*order, table.Columns[i]...)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// noteRows is a list of all the note rows, implementing ListData & MutableListData interfaces
|
|
||||||
type noteRows Model
|
|
||||||
|
|
||||||
func (m *Model) NoteRows() List { return List{(*noteRows)(m)} }
|
|
||||||
|
|
||||||
func (n *noteRows) Count() int { return n.d.Song.Score.Length * n.d.Song.Score.RowsPerPattern }
|
|
||||||
func (n *noteRows) Selected() int { return n.d.Song.Score.SongRow(n.d.Cursor.SongPos) }
|
|
||||||
func (n *noteRows) Selected2() int { return n.d.Song.Score.SongRow(n.d.Cursor2.SongPos) }
|
|
||||||
func (n *noteRows) SetSelected2(v int) { n.d.Cursor2.SongPos = n.d.Song.Score.SongPos(v) }
|
|
||||||
func (n *noteRows) SetSelected(value int) {
|
|
||||||
if value != n.d.Song.Score.SongRow(n.d.Cursor.SongPos) {
|
|
||||||
n.follow = false
|
|
||||||
}
|
|
||||||
n.d.Cursor.SongPos = n.d.Song.Score.Clamp(n.d.Song.Score.SongPos(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *noteRows) Move(r Range, delta int) (ok bool) {
|
|
||||||
for a, b := range r.Swaps(delta) {
|
|
||||||
apos := v.d.Song.Score.SongPos(a)
|
|
||||||
bpos := v.d.Song.Score.SongPos(b)
|
|
||||||
for _, t := range v.d.Song.Score.Tracks {
|
|
||||||
n1 := t.Note(apos)
|
|
||||||
n2 := t.Note(bpos)
|
|
||||||
t.SetNote(apos, n2, v.uniquePatterns)
|
|
||||||
t.SetNote(bpos, n1, v.uniquePatterns)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *noteRows) Delete(r Range) (ok bool) {
|
|
||||||
for _, track := range v.d.Song.Score.Tracks {
|
|
||||||
for i := r.Start; i < r.End; i++ {
|
|
||||||
pos := v.d.Song.Score.SongPos(i)
|
|
||||||
track.SetNote(pos, 1, v.uniquePatterns)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *noteRows) Change(n string, severity ChangeSeverity) func() {
|
|
||||||
return (*Model)(v).change("NoteRowList."+n, ScoreChange, severity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *noteRows) Cancel() {
|
|
||||||
(*Model)(v).changeCancel = true
|
|
||||||
}
|
|
||||||
|
|
||||||
type marshalNoteRows struct {
|
|
||||||
NoteRows [][]byte `yaml:",flow"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *noteRows) Marshal(r Range) ([]byte, error) {
|
|
||||||
var table marshalNoteRows
|
|
||||||
for i, track := range v.d.Song.Score.Tracks {
|
|
||||||
table.NoteRows = append(table.NoteRows, make([]byte, r.Len()))
|
|
||||||
for j := 0; j < r.Len(); j++ {
|
|
||||||
row := r.Start + j
|
|
||||||
pos := v.d.Song.Score.SongPos(row)
|
|
||||||
table.NoteRows[i][j] = track.Note(pos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return yaml.Marshal(table)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *noteRows) Unmarshal(data []byte) (r Range, err error) {
|
|
||||||
var table marshalNoteRows
|
|
||||||
if err := yaml.Unmarshal(data, &table); err != nil {
|
|
||||||
return Range{}, fmt.Errorf("NoteRowList.unmarshal: %v", err)
|
|
||||||
}
|
|
||||||
if len(table.NoteRows) < 1 {
|
|
||||||
return Range{}, errors.New("NoteRowList.unmarshal: no tracks")
|
|
||||||
}
|
|
||||||
r.Start = v.d.Song.Score.SongRow(v.d.Cursor.SongPos)
|
|
||||||
for i, arr := range table.NoteRows {
|
|
||||||
if i >= len(v.d.Song.Score.Tracks) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r.End = r.Start + len(arr)
|
|
||||||
for j, note := range arr {
|
|
||||||
y := j + r.Start
|
|
||||||
pos := v.d.Song.Score.SongPos(y)
|
|
||||||
v.d.Song.Score.Tracks[i].SetNote(pos, note, v.uniquePatterns)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// searchResults is a unmutable list of all the search results, implementing ListData interface
|
|
||||||
type (
|
|
||||||
searchResults Model
|
|
||||||
UnitSearchYieldFunc func(index int, item string) (ok bool)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *Model) SearchResults() List { return List{(*searchResults)(m)} }
|
|
||||||
func (l *Model) SearchResult(i int) (name string, ok bool) {
|
|
||||||
if i < 0 || i >= len(l.derived.searchResults) {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return l.derived.searchResults[i], true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *searchResults) Selected() int { return l.d.UnitSearchIndex }
|
|
||||||
func (l *searchResults) Selected2() int { return l.d.UnitSearchIndex }
|
|
||||||
func (l *searchResults) SetSelected(value int) { l.d.UnitSearchIndex = value }
|
|
||||||
func (l *searchResults) SetSelected2(value int) {}
|
|
||||||
func (l *searchResults) Count() (count int) { return len(l.derived.searchResults) }
|
|
||||||
|
|
||||||
func (r Range) Len() int { return r.End - r.Start }
|
|
||||||
|
|
||||||
func (r Range) Swaps(delta int) iter.Seq2[int, int] {
|
|
||||||
if delta > 0 {
|
|
||||||
return func(yield func(int, int) bool) {
|
|
||||||
for i := r.End - 1; i >= r.Start; i-- {
|
|
||||||
if !yield(i, i+delta) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return func(yield func(int, int) bool) {
|
|
||||||
for i := r.Start; i < r.End; i++ {
|
|
||||||
if !yield(i, i+delta) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Range) Intersect(s Range) (ret Range) {
|
|
||||||
ret.Start = max(r.Start, s.Start)
|
|
||||||
ret.End = max(min(r.End, s.End), ret.Start)
|
|
||||||
if ret.Len() == 0 {
|
|
||||||
return Range{}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func MakeMoveRanges(a Range, delta int) [4]Range {
|
|
||||||
if delta < 0 {
|
|
||||||
return [4]Range{
|
|
||||||
{math.MinInt, a.Start + delta},
|
|
||||||
{a.Start, a.End},
|
|
||||||
{a.Start + delta, a.Start},
|
|
||||||
{a.End, math.MaxInt},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [4]Range{
|
|
||||||
{math.MinInt, a.Start},
|
|
||||||
{a.End, a.End + delta},
|
|
||||||
{a.Start, a.End},
|
|
||||||
{a.End + delta, math.MaxInt},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeSetLength takes a range and a length, and returns a slice of ranges that
|
|
||||||
// can be used with VoiceSlice to expand or shrink the range to the given
|
|
||||||
// length, by either duplicating or removing elements. The function tries to
|
|
||||||
// duplicate elements so all elements are equally spaced, and tries to remove
|
|
||||||
// elements from the middle of the range.
|
|
||||||
func MakeSetLength(a Range, length int) []Range {
|
|
||||||
if length <= 0 || a.Len() <= 0 {
|
|
||||||
return []Range{{a.Start, a.Start}}
|
|
||||||
}
|
|
||||||
ret := make([]Range, a.Len(), max(a.Len(), length)+2)
|
|
||||||
for i := 0; i < a.Len(); i++ {
|
|
||||||
ret[i] = Range{a.Start + i, a.Start + i + 1}
|
|
||||||
}
|
|
||||||
for x := len(ret); x < length; x++ {
|
|
||||||
e := (x << 1) ^ (1 << bits.Len((uint)(x)))
|
|
||||||
ret = append(ret[0:e+1], ret[e:]...)
|
|
||||||
}
|
|
||||||
for x := len(ret); x > length; x-- {
|
|
||||||
e := (((x << 1) ^ (1 << bits.Len((uint)(x)))) + x - 1) % x
|
|
||||||
ret = append(ret[0:e], ret[e+1:]...)
|
|
||||||
}
|
|
||||||
ret = append([]Range{{math.MinInt, a.Start}}, ret...)
|
|
||||||
ret = append(ret, Range{a.End, math.MaxInt})
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func Complement(a Range) [2]Range {
|
|
||||||
return [2]Range{
|
|
||||||
{math.MinInt, a.Start},
|
|
||||||
{a.End, math.MaxInt},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert inserts elements into a slice at the given index. If the index is out
|
|
||||||
// of bounds, the function returns false.
|
|
||||||
func Insert[T any, S ~[]T](slice S, index int, inserted ...T) (ret S, ok bool) {
|
|
||||||
if index < 0 || index > len(slice) {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
ret = make(S, 0, len(slice)+len(inserted))
|
|
||||||
ret = append(ret, slice[:index]...)
|
|
||||||
ret = append(ret, inserted...)
|
|
||||||
ret = append(ret, slice[index:]...)
|
|
||||||
return ret, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// VoiceSlice works similar to the Slice function, but takes a slice of
|
|
||||||
// NumVoicer:s and treats it as a "virtual slice", with element repeated by the
|
|
||||||
// number of voices it has. NumVoicer interface is implemented at least by
|
|
||||||
// sointu.Tracks and sointu.Instruments. For example, if parameter "slice" has
|
|
||||||
// three elements, returning GetNumVoices 2, 1, and 3, the VoiceSlice thinks of
|
|
||||||
// this as a virtual slice of 6 elements [0,0,1,2,2,2]. Then, the "ranges"
|
|
||||||
// parameter are slicing ranges to this virtual slice. Continuing with the
|
|
||||||
// example, if "ranges" was [2,5), the virtual slice would be [1,2,2], and the
|
|
||||||
// function would return a slice with two elements: first with NumVoices 1 and
|
|
||||||
// second with NumVoices 2. If multiple ranges are given, multiple virtual
|
|
||||||
// slices are concatenated. However, when doing so, splitting an element is not
|
|
||||||
// allowed. In the previous example, if the ranges were [1,3) and [0,1), the
|
|
||||||
// resulting concatenated virtual slice would be [0,1,0], and here the 0 element
|
|
||||||
// would be split. This is to avoid accidentally making shallow copies of
|
|
||||||
// reference types.
|
|
||||||
func VoiceSlice[T any, S ~[]T, P sointu.NumVoicerPointer[T]](slice S, ranges ...Range) (ret S, ok bool) {
|
|
||||||
ret = make(S, 0, len(slice))
|
|
||||||
last := -1
|
|
||||||
used := make([]bool, len(slice))
|
|
||||||
outer:
|
|
||||||
for _, r := range ranges {
|
|
||||||
left := 0
|
|
||||||
for i, elem := range slice {
|
|
||||||
right := left + (P)(&slice[i]).GetNumVoices()
|
|
||||||
if left >= r.End {
|
|
||||||
continue outer
|
|
||||||
}
|
|
||||||
if right <= r.Start {
|
|
||||||
left = right
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
overlap := min(right, r.End) - max(left, r.Start)
|
|
||||||
if last == i {
|
|
||||||
(P)(&ret[len(ret)-1]).SetNumVoices(
|
|
||||||
(P)(&ret[len(ret)-1]).GetNumVoices() + overlap)
|
|
||||||
} else {
|
|
||||||
if last == math.MaxInt || used[i] {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
ret = append(ret, elem)
|
|
||||||
(P)(&ret[len(ret)-1]).SetNumVoices(overlap)
|
|
||||||
used[i] = true
|
|
||||||
}
|
|
||||||
last = i
|
|
||||||
left = right
|
|
||||||
}
|
|
||||||
if left >= r.End {
|
|
||||||
continue outer
|
|
||||||
}
|
|
||||||
last = math.MaxInt // the list is closed, adding more elements causes it to fail
|
|
||||||
}
|
|
||||||
return ret, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// VoiceInsert tries adding the elements "added" to the slice "orig" at the
|
|
||||||
// voice index "index". Notice that index is the index into a virtual slice
|
|
||||||
// where each element is repeated by the number of voices it has. If the index
|
|
||||||
// is between elements, the new elements are added in between the old elements.
|
|
||||||
// If the addition would cause splitting of an element, we rather increase the
|
|
||||||
// number of voices the element has, but do not split it.
|
|
||||||
func VoiceInsert[T any, S ~[]T, P sointu.NumVoicerPointer[T]](orig S, index, length int, added ...T) (ret S, retRange Range, ok bool) {
|
|
||||||
ret = make(S, 0, len(orig)+length)
|
|
||||||
left := 0
|
|
||||||
for i, elem := range orig {
|
|
||||||
right := left + (P)(&orig[i]).GetNumVoices()
|
|
||||||
if left == index { // we are between elements and it's safe to add there
|
|
||||||
if sointu.TotalVoices[T, S, P](added) < length {
|
|
||||||
return nil, Range{}, false // we are missing some elements
|
|
||||||
}
|
|
||||||
retRange = Range{len(ret), len(ret) + len(added)}
|
|
||||||
ret = append(ret, added...)
|
|
||||||
} else if left < index && index < right { // we are inside an element and would split it; just increase its voices instead of splitting
|
|
||||||
(P)(&elem).SetNumVoices((P)(&orig[i]).GetNumVoices() + sointu.TotalVoices[T, S, P](added))
|
|
||||||
retRange = Range{len(ret), len(ret)}
|
|
||||||
}
|
|
||||||
ret = append(ret, elem)
|
|
||||||
left = right
|
|
||||||
}
|
|
||||||
if left == index { // we are at the end and it's safe to add there, even if we are missing some elements
|
|
||||||
retRange = Range{len(ret), len(ret) + len(added)}
|
|
||||||
ret = append(ret, added...)
|
|
||||||
}
|
|
||||||
return ret, retRange, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func VoiceRange[T any, S ~[]T, P sointu.NumVoicerPointer[T]](slice S, indexRange Range) (voiceRange Range) {
|
|
||||||
indexRange.Start = max(0, indexRange.Start)
|
|
||||||
indexRange.End = min(len(slice), indexRange.End)
|
|
||||||
for _, e := range slice[:indexRange.Start] {
|
|
||||||
voiceRange.Start += (P)(&e).GetNumVoices()
|
|
||||||
}
|
|
||||||
voiceRange.End = voiceRange.Start
|
|
||||||
for i := indexRange.Start; i < indexRange.End; i++ {
|
|
||||||
voiceRange.End += (P)(&slice[i]).GetNumVoices()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// helpers
|
|
||||||
|
|
||||||
func (m *Model) sliceInstrumentsTracks(instruments, tracks bool, ranges ...Range) (ok bool) {
|
|
||||||
defer m.change("sliceInstrumentsTracks", PatchChange, MajorChange)()
|
|
||||||
if instruments {
|
|
||||||
m.d.Song.Patch, ok = VoiceSlice(m.d.Song.Patch, ranges...)
|
|
||||||
if !ok {
|
|
||||||
goto fail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tracks {
|
|
||||||
m.d.Song.Score.Tracks, ok = VoiceSlice(m.d.Song.Score.Tracks, ranges...)
|
|
||||||
if !ok {
|
|
||||||
goto fail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
fail:
|
|
||||||
(*Model)(m).Alerts().AddNamed("slicesInstrumentsTracks", "Modify prevented by Instrument-Track linking", Warning)
|
|
||||||
m.changeCancel = true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) marshalVoices(r Range) (data []byte, err error) {
|
|
||||||
patch, ok := VoiceSlice(m.d.Song.Patch, r)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("marshalVoiceRange: slicing patch failed")
|
|
||||||
}
|
|
||||||
tracks, ok := VoiceSlice(m.d.Song.Score.Tracks, r)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("marshalVoiceRange: slicing tracks failed")
|
|
||||||
}
|
|
||||||
return yaml.Marshal(struct {
|
|
||||||
Patch sointu.Patch
|
|
||||||
Tracks []sointu.Track
|
|
||||||
}{patch, tracks})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) unmarshalVoices(voiceIndex int, data []byte, instruments, tracks bool) (instrRange, trackRange Range, ok bool) {
|
|
||||||
var d struct {
|
|
||||||
Patch sointu.Patch
|
|
||||||
Tracks []sointu.Track
|
|
||||||
}
|
|
||||||
if err := yaml.Unmarshal(data, &d); err != nil {
|
|
||||||
return Range{}, Range{}, false
|
|
||||||
}
|
|
||||||
return m.addVoices(voiceIndex, d.Patch, d.Tracks, instruments, tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) addVoices(voiceIndex int, p sointu.Patch, t []sointu.Track, instruments, tracks bool) (instrRange Range, trackRange Range, ok bool) {
|
|
||||||
defer m.change("addVoices", PatchChange, MajorChange)()
|
|
||||||
addedLength := max(p.NumVoices(), sointu.TotalVoices(t))
|
|
||||||
if instruments {
|
|
||||||
m.assignUnitIDsForPatch(p)
|
|
||||||
m.d.Song.Patch, instrRange, ok = VoiceInsert(m.d.Song.Patch, voiceIndex, addedLength, p...)
|
|
||||||
if !ok {
|
|
||||||
goto fail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tracks {
|
|
||||||
m.d.Song.Score.Tracks, trackRange, ok = VoiceInsert(m.d.Song.Score.Tracks, voiceIndex, addedLength, t...)
|
|
||||||
if !ok {
|
|
||||||
goto fail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instrRange, trackRange, true
|
|
||||||
fail:
|
|
||||||
(*Model)(m).Alerts().AddNamed("addVoices", "Adding voices prevented by Instrument-Track linking", Warning)
|
|
||||||
m.changeCancel = true
|
|
||||||
return Range{}, Range{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) remainingVoices(instruments, tracks bool) (ret int) {
|
|
||||||
ret = math.MaxInt
|
|
||||||
if instruments {
|
|
||||||
ret = min(ret, vm.MAX_VOICES-m.d.Song.Patch.NumVoices())
|
|
||||||
}
|
|
||||||
if tracks {
|
|
||||||
ret = min(ret, vm.MAX_VOICES-m.d.Song.Score.NumVoices())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
77
tracker/midi.go
Normal file
77
tracker/midi.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
MIDIModel Model
|
||||||
|
MIDIContext interface {
|
||||||
|
InputDevices(yield func(deviceName string) bool)
|
||||||
|
Open(deviceName string) error
|
||||||
|
Close()
|
||||||
|
IsOpen() bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Model) MIDI() *MIDIModel { return (*MIDIModel)(m) }
|
||||||
|
|
||||||
|
// InputDevices can be iterated to get string names of all the MIDI input
|
||||||
|
// devices.
|
||||||
|
func (m *MIDIModel) InputDevices(yield func(deviceName string) bool) { m.midi.InputDevices(yield) }
|
||||||
|
|
||||||
|
// IsOpen returns true if a midi device is currently open.
|
||||||
|
func (m *MIDIModel) IsOpen() bool { return m.midi.IsOpen() }
|
||||||
|
|
||||||
|
// InputtingNotes returns a Bool controlling whether the MIDI events are used
|
||||||
|
// just to trigger instruments, or if the note events are used to input notes to
|
||||||
|
// the note table.
|
||||||
|
func (m *MIDIModel) InputtingNotes() Bool { return MakeBool((*midiInputtingNotes)(m)) }
|
||||||
|
|
||||||
|
type midiInputtingNotes Model
|
||||||
|
|
||||||
|
func (m *midiInputtingNotes) Value() bool { return m.broker.mIDIEventsToGUI.Load() }
|
||||||
|
func (m *midiInputtingNotes) SetValue(val bool) { m.broker.mIDIEventsToGUI.Store(val) }
|
||||||
|
|
||||||
|
// Open returns an Action to open the MIDI input device with a given name.
|
||||||
|
func (m *MIDIModel) Open(deviceName string) Action {
|
||||||
|
return MakeAction(openMIDI{Item: deviceName, Model: (*Model)(m)})
|
||||||
|
}
|
||||||
|
|
||||||
|
type openMIDI struct {
|
||||||
|
Item string
|
||||||
|
*Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s openMIDI) Do() {
|
||||||
|
m := s.Model
|
||||||
|
if err := s.Model.midi.Open(s.Item); err == nil {
|
||||||
|
message := fmt.Sprintf("Opened MIDI device: %s", s.Item)
|
||||||
|
m.Alerts().Add(message, Info)
|
||||||
|
} else {
|
||||||
|
message := fmt.Sprintf("Could not open MIDI device: %s", s.Item)
|
||||||
|
m.Alerts().Add(message, Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindMIDIDeviceByPrefix finds the MIDI input device whose name starts with the given
|
||||||
|
// prefix. It returns the full device name and true if found, or an empty string
|
||||||
|
// and false if not found.
|
||||||
|
func FindMIDIDeviceByPrefix(c MIDIContext, prefix string) (deviceName string, ok bool) {
|
||||||
|
for input := range c.InputDevices {
|
||||||
|
if strings.HasPrefix(input, prefix) {
|
||||||
|
return input, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NullMIDIContext is a mockup MIDIContext if you don't want to create a real
|
||||||
|
// one.
|
||||||
|
type NullMIDIContext struct{}
|
||||||
|
|
||||||
|
func (m NullMIDIContext) InputDevices(yield func(string) bool) {}
|
||||||
|
func (m NullMIDIContext) Open(deviceName string) error { return nil }
|
||||||
|
func (m NullMIDIContext) Close() {}
|
||||||
|
func (m NullMIDIContext) IsOpen() bool { return false }
|
||||||
191
tracker/model.go
191
tracker/model.go
@@ -2,11 +2,8 @@ package tracker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"time"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
)
|
)
|
||||||
@@ -45,7 +42,7 @@ type (
|
|||||||
d modelData
|
d modelData
|
||||||
derived derivedModelData
|
derived derivedModelData
|
||||||
|
|
||||||
instrEnlarged bool
|
trackerHidden bool
|
||||||
|
|
||||||
prevUndoKind string
|
prevUndoKind string
|
||||||
undoSkipCounter int
|
undoSkipCounter int
|
||||||
@@ -71,7 +68,7 @@ type (
|
|||||||
|
|
||||||
playerStatus PlayerStatus
|
playerStatus PlayerStatus
|
||||||
|
|
||||||
signalAnalyzer *ScopeModel
|
scopeData scopeData
|
||||||
detectorResult DetectorResult
|
detectorResult DetectorResult
|
||||||
|
|
||||||
spectrum *Spectrum
|
spectrum *Spectrum
|
||||||
@@ -79,7 +76,7 @@ type (
|
|||||||
weightingType WeightingType
|
weightingType WeightingType
|
||||||
oversampling bool
|
oversampling bool
|
||||||
|
|
||||||
specAnSettings SpecAnSettings
|
specAnSettings specAnSettings
|
||||||
specAnEnabled bool
|
specAnEnabled bool
|
||||||
|
|
||||||
alerts []Alert
|
alerts []Alert
|
||||||
@@ -90,10 +87,9 @@ type (
|
|||||||
|
|
||||||
broker *Broker
|
broker *Broker
|
||||||
|
|
||||||
MIDI MIDIContext
|
midi MIDIContext
|
||||||
|
|
||||||
presets Presets
|
presetData presetData
|
||||||
presetIndex int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor identifies a row and a track in a song score.
|
// Cursor identifies a row and a track in a song score.
|
||||||
@@ -126,15 +122,6 @@ type (
|
|||||||
|
|
||||||
Dialog int
|
Dialog int
|
||||||
|
|
||||||
MIDIContext interface {
|
|
||||||
InputDevices(yield func(string) bool)
|
|
||||||
Open(name string) error
|
|
||||||
Close()
|
|
||||||
IsOpen() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
NullMIDIContext struct{}
|
|
||||||
|
|
||||||
InstrumentTab int
|
InstrumentTab int
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -174,31 +161,25 @@ const (
|
|||||||
InstrumentEditorTab InstrumentTab = iota
|
InstrumentEditorTab InstrumentTab = iota
|
||||||
InstrumentPresetsTab
|
InstrumentPresetsTab
|
||||||
InstrumentCommentTab
|
InstrumentCommentTab
|
||||||
|
NumInstrumentTabs
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxUndo = 64
|
const maxUndo = 64
|
||||||
|
|
||||||
func (m *Model) PlayPosition() sointu.SongPos { return m.playerStatus.SongPos }
|
func (m *Model) Dialog() Dialog { return m.dialog }
|
||||||
func (m *Model) Loop() Loop { return m.loop }
|
func (m *Model) Quitted() bool { return m.quitted }
|
||||||
func (m *Model) PlaySongRow() int { return m.d.Song.Score.SongRow(m.playerStatus.SongPos) }
|
|
||||||
func (m *Model) ChangedSinceSave() bool { return m.d.ChangedSinceSave }
|
|
||||||
func (m *Model) Dialog() Dialog { return m.dialog }
|
|
||||||
func (m *Model) Quitted() bool { return m.quitted }
|
|
||||||
|
|
||||||
func (m *Model) DetectorResult() DetectorResult { return m.detectorResult }
|
|
||||||
func (m *Model) Spectrum() Spectrum { return *m.spectrum }
|
|
||||||
|
|
||||||
// NewModelPlayer creates a new model and a player that communicates with it
|
// NewModelPlayer creates a new model and a player that communicates with it
|
||||||
func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext, recoveryFilePath string) *Model {
|
func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext, recoveryFilePath string) *Model {
|
||||||
m := new(Model)
|
m := new(Model)
|
||||||
m.synthers = synthers
|
m.synthers = synthers
|
||||||
m.MIDI = midiContext
|
m.midi = midiContext
|
||||||
m.broker = broker
|
m.broker = broker
|
||||||
m.d.Octave = 4
|
m.d.Octave = 4
|
||||||
m.linkInstrTrack = true
|
m.linkInstrTrack = true
|
||||||
m.d.RecoveryFilePath = recoveryFilePath
|
m.d.RecoveryFilePath = recoveryFilePath
|
||||||
m.spectrum = broker.GetSpectrum()
|
m.spectrum = broker.GetSpectrum()
|
||||||
m.resetSong()
|
m.Song().reset()
|
||||||
if recoveryFilePath != "" {
|
if recoveryFilePath != "" {
|
||||||
if bytes2, err := os.ReadFile(m.d.RecoveryFilePath); err == nil {
|
if bytes2, err := os.ReadFile(m.d.RecoveryFilePath); err == nil {
|
||||||
var data modelData
|
var data modelData
|
||||||
@@ -208,24 +189,60 @@ func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TrySend(broker.ToPlayer, any(m.d.Song.Copy())) // we should be non-blocking in the constructor
|
TrySend(broker.ToPlayer, any(m.d.Song.Copy())) // we should be non-blocking in the constructor
|
||||||
m.signalAnalyzer = NewScopeModel(m.d.Song.BPM)
|
m.scopeData = scopeData{lengthInBeats: 4}
|
||||||
|
m.Scope().updateBufferLength()
|
||||||
m.updateDeriveData(SongChange)
|
m.updateDeriveData(SongChange)
|
||||||
m.presets.load()
|
m.presetData.load()
|
||||||
m.updateDerivedPresetSearch()
|
m.Preset().updateCache()
|
||||||
m.derived.searchResults = make([]string, 0, len(sointu.UnitNames))
|
m.derived.searchResults = make([]string, 0, len(sointu.UnitNames))
|
||||||
m.updateDerivedUnitSearch()
|
m.Unit().updateDerivedUnitSearch()
|
||||||
|
go runDetector(broker)
|
||||||
|
go runSpecAnalyzer(broker)
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindMIDIDeviceByPrefix(c MIDIContext, prefix string) (input string, ok bool) {
|
func (m *Model) Close() {
|
||||||
for input := range c.InputDevices {
|
TrySend(m.broker.CloseDetector, struct{}{})
|
||||||
if strings.HasPrefix(input, prefix) {
|
TrySend(m.broker.CloseSpecAn, struct{}{})
|
||||||
return input, true
|
TimeoutReceive(m.broker.FinishedDetector, 3*time.Second)
|
||||||
}
|
TimeoutReceive(m.broker.FinishedSpecAn, 3*time.Second)
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestQuit asks the tracker to quit, showing a dialog if there are unsaved
|
||||||
|
// changes.
|
||||||
|
func (m *Model) RequestQuit() Action { return MakeAction((*requestQuit)(m)) }
|
||||||
|
|
||||||
|
type requestQuit Model
|
||||||
|
|
||||||
|
func (m *requestQuit) Do() {
|
||||||
|
if !m.quitted {
|
||||||
|
m.dialog = QuitChanges
|
||||||
|
(*SongModel)(m).completeAction(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForceQuit returns an Action to force the tracker to quit immediately, without
|
||||||
|
// saving any changes.
|
||||||
|
func (m *Model) ForceQuit() Action { return MakeAction((*forceQuit)(m)) }
|
||||||
|
|
||||||
|
type forceQuit Model
|
||||||
|
|
||||||
|
func (m *forceQuit) Do() { m.quitted = true }
|
||||||
|
|
||||||
|
// ShowLicense returns an Action to show the software license dialog.
|
||||||
|
func (m *Model) ShowLicense() Action { return MakeAction((*showLicense)(m)) }
|
||||||
|
|
||||||
|
type showLicense Model
|
||||||
|
|
||||||
|
func (m *showLicense) Do() { m.dialog = License }
|
||||||
|
|
||||||
|
// CancelDialog returns an Action to cancel the current dialog.
|
||||||
|
func (m *Model) CancelDialog() Action { return MakeAction((*cancelDialog)(m)) }
|
||||||
|
|
||||||
|
type cancelDialog Model
|
||||||
|
|
||||||
|
func (m *cancelDialog) Do() { m.dialog = NoDialog }
|
||||||
|
|
||||||
func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func() {
|
func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func() {
|
||||||
if m.changeLevel == 0 {
|
if m.changeLevel == 0 {
|
||||||
m.changeType = NoChange
|
m.changeType = NoChange
|
||||||
@@ -276,7 +293,7 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func(
|
|||||||
}
|
}
|
||||||
if m.changeType&BPMChange != 0 {
|
if m.changeType&BPMChange != 0 {
|
||||||
TrySend(m.broker.ToPlayer, any(BPMMsg{m.d.Song.BPM}))
|
TrySend(m.broker.ToPlayer, any(BPMMsg{m.d.Song.BPM}))
|
||||||
m.signalAnalyzer.SetBpm(m.d.Song.BPM)
|
m.Scope().updateBufferLength()
|
||||||
}
|
}
|
||||||
if m.changeType&RowsPerBeatChange != 0 {
|
if m.changeType&RowsPerBeatChange != 0 {
|
||||||
TrySend(m.broker.ToPlayer, any(RowsPerBeatMsg{m.d.Song.RowsPerBeat}))
|
TrySend(m.broker.ToPlayer, any(RowsPerBeatMsg{m.d.Song.RowsPerBeat}))
|
||||||
@@ -306,65 +323,6 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) MarshalRecovery() []byte {
|
|
||||||
out, err := json.Marshal(m.d)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if m.d.RecoveryFilePath != "" {
|
|
||||||
os.Remove(m.d.RecoveryFilePath)
|
|
||||||
}
|
|
||||||
m.d.ChangedSinceRecovery = false
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SaveRecovery() error {
|
|
||||||
if !m.d.ChangedSinceRecovery {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if m.d.RecoveryFilePath == "" {
|
|
||||||
return errors.New("no backup file path")
|
|
||||||
}
|
|
||||||
out, err := json.Marshal(m.d)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not marshal recovery data: %w", err)
|
|
||||||
}
|
|
||||||
dir := filepath.Dir(m.d.RecoveryFilePath)
|
|
||||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
|
||||||
os.MkdirAll(dir, os.ModePerm)
|
|
||||||
}
|
|
||||||
file, err := os.Create(m.d.RecoveryFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not create recovery file: %w", err)
|
|
||||||
}
|
|
||||||
_, err = file.Write(out)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not write recovery file: %w", err)
|
|
||||||
}
|
|
||||||
m.d.ChangedSinceRecovery = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) UnmarshalRecovery(bytes []byte) {
|
|
||||||
var data modelData
|
|
||||||
err := json.Unmarshal(bytes, &data)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.d = data
|
|
||||||
if m.d.RecoveryFilePath != "" { // check if there's a recovery file on disk and load it instead
|
|
||||||
if bytes2, err := os.ReadFile(m.d.RecoveryFilePath); err == nil {
|
|
||||||
var data modelData
|
|
||||||
if json.Unmarshal(bytes2, &data) == nil {
|
|
||||||
m.d = data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.d.ChangedSinceRecovery = false
|
|
||||||
TrySend(m.broker.ToPlayer, any(m.d.Song.Copy()))
|
|
||||||
m.updateDeriveData(SongChange)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) ProcessMsg(msg MsgToModel) {
|
func (m *Model) ProcessMsg(msg MsgToModel) {
|
||||||
if msg.HasPanicPlayerStatus {
|
if msg.HasPanicPlayerStatus {
|
||||||
m.playerStatus = msg.PlayerStatus
|
m.playerStatus = msg.PlayerStatus
|
||||||
@@ -373,7 +331,7 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
|||||||
m.d.Cursor2.SongPos = msg.PlayerStatus.SongPos
|
m.d.Cursor2.SongPos = msg.PlayerStatus.SongPos
|
||||||
TrySend(m.broker.ToGUI, any(MsgToGUI{
|
TrySend(m.broker.ToGUI, any(MsgToGUI{
|
||||||
Kind: GUIMessageCenterOnRow,
|
Kind: GUIMessageCenterOnRow,
|
||||||
Param: m.PlaySongRow(),
|
Param: m.Play().SongRow(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
m.panic = msg.Panic
|
m.panic = msg.Panic
|
||||||
@@ -382,10 +340,10 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
|||||||
m.detectorResult = msg.DetectorResult
|
m.detectorResult = msg.DetectorResult
|
||||||
}
|
}
|
||||||
if msg.TriggerChannel > 0 {
|
if msg.TriggerChannel > 0 {
|
||||||
m.signalAnalyzer.Trigger(msg.TriggerChannel)
|
m.Scope().trigger(msg.TriggerChannel)
|
||||||
}
|
}
|
||||||
if msg.Reset {
|
if msg.Reset {
|
||||||
m.signalAnalyzer.Reset()
|
m.Scope().reset()
|
||||||
TrySend(m.broker.ToDetector, MsgToDetector{Reset: true}) // chain the messages: when the signal analyzer is reset, also reset the detector
|
TrySend(m.broker.ToDetector, MsgToDetector{Reset: true}) // chain the messages: when the signal analyzer is reset, also reset the detector
|
||||||
}
|
}
|
||||||
switch e := msg.Data.(type) {
|
switch e := msg.Data.(type) {
|
||||||
@@ -402,13 +360,13 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
|||||||
defer m.change("Recording", SongChange, MajorChange)()
|
defer m.change("Recording", SongChange, MajorChange)()
|
||||||
m.d.Song.Score = score
|
m.d.Song.Score = score
|
||||||
m.d.Song.BPM = int(e.BPM + 0.5)
|
m.d.Song.BPM = int(e.BPM + 0.5)
|
||||||
m.instrEnlarged = false
|
m.trackerHidden = false
|
||||||
case Alert:
|
case Alert:
|
||||||
m.Alerts().AddAlert(e)
|
m.Alerts().AddAlert(e)
|
||||||
case IsPlayingMsg:
|
case IsPlayingMsg:
|
||||||
m.playing = e.bool
|
m.playing = e.bool
|
||||||
case *sointu.AudioBuffer:
|
case *sointu.AudioBuffer:
|
||||||
m.signalAnalyzer.ProcessAudioBuffer(e)
|
m.Scope().processAudioBuffer(e)
|
||||||
// chain the messages: when we have a new audio buffer, send them to the detector and the spectrum analyzer
|
// chain the messages: when we have a new audio buffer, send them to the detector and the spectrum analyzer
|
||||||
if m.specAnEnabled { // send buffers to spectrum analyzer only if it's enabled
|
if m.specAnEnabled { // send buffers to spectrum analyzer only if it's enabled
|
||||||
clone := m.broker.GetAudioBuffer()
|
clone := m.broker.GetAudioBuffer()
|
||||||
@@ -426,12 +384,7 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) CPULoad(buf []sointu.CPULoad) int {
|
func (m *Model) Broker() *Broker { return m.broker }
|
||||||
return copy(buf, m.playerStatus.CPULoad[:m.playerStatus.NumThreads])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) SignalAnalyzer() *ScopeModel { return m.signalAnalyzer }
|
|
||||||
func (m *Model) Broker() *Broker { return m.broker }
|
|
||||||
|
|
||||||
func (d *modelData) Copy() modelData {
|
func (d *modelData) Copy() modelData {
|
||||||
ret := *d
|
ret := *d
|
||||||
@@ -439,20 +392,6 @@ func (d *modelData) Copy() modelData {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m NullMIDIContext) InputDevices(yield func(string) bool) {}
|
|
||||||
func (m NullMIDIContext) Open(name string) error { return nil }
|
|
||||||
func (m NullMIDIContext) Close() {}
|
|
||||||
func (m NullMIDIContext) IsOpen() bool { return false }
|
|
||||||
|
|
||||||
func (m *Model) resetSong() {
|
|
||||||
m.d.Song = defaultSong.Copy()
|
|
||||||
for _, instr := range m.d.Song.Patch {
|
|
||||||
(*Model)(m).assignUnitIDs(instr.Units)
|
|
||||||
}
|
|
||||||
m.d.FilePath = ""
|
|
||||||
m.d.ChangedSinceSave = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) maxID() int {
|
func (m *Model) maxID() int {
|
||||||
maxID := 0
|
maxID := 0
|
||||||
for _, instr := range m.d.Song.Patch {
|
for _, instr := range m.d.Song.Patch {
|
||||||
|
|||||||
@@ -35,88 +35,88 @@ func (mwc *myWriteCloser) Close() error {
|
|||||||
|
|
||||||
func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||||
// Ints
|
// Ints
|
||||||
s.IterateInt("InstrumentVoices", s.model.InstrumentVoices(), yield, seed)
|
s.IterateInt("InstrumentVoices", s.model.Instrument().Voices(), yield, seed)
|
||||||
s.IterateInt("TrackVoices", s.model.TrackVoices(), yield, seed)
|
s.IterateInt("TrackVoices", s.model.Track().Voices(), yield, seed)
|
||||||
s.IterateInt("SongLength", s.model.SongLength(), yield, seed)
|
s.IterateInt("SongLength", s.model.Song().Length(), yield, seed)
|
||||||
s.IterateInt("BPM", s.model.BPM(), yield, seed)
|
s.IterateInt("BPM", s.model.Song().BPM(), yield, seed)
|
||||||
s.IterateInt("RowsPerPattern", s.model.RowsPerPattern(), yield, seed)
|
s.IterateInt("RowsPerPattern", s.model.Song().RowsPerPattern(), yield, seed)
|
||||||
s.IterateInt("RowsPerBeat", s.model.RowsPerBeat(), yield, seed)
|
s.IterateInt("RowsPerBeat", s.model.Song().RowsPerBeat(), yield, seed)
|
||||||
s.IterateInt("Step", s.model.Step(), yield, seed)
|
s.IterateInt("Step", s.model.Note().Step(), yield, seed)
|
||||||
s.IterateInt("Octave", s.model.Octave(), yield, seed)
|
s.IterateInt("Octave", s.model.Note().Octave(), yield, seed)
|
||||||
// Lists
|
// Lists
|
||||||
s.IterateList("Instruments", s.model.Instruments(), yield, seed)
|
s.IterateList("Instruments", s.model.Instrument().List(), yield, seed)
|
||||||
s.IterateList("Units", s.model.Units(), yield, seed)
|
s.IterateList("Units", s.model.Unit().List(), yield, seed)
|
||||||
s.IterateList("Tracks", s.model.Tracks(), yield, seed)
|
s.IterateList("Tracks", s.model.Track().List(), yield, seed)
|
||||||
s.IterateList("OrderRows", s.model.OrderRows(), yield, seed)
|
s.IterateList("OrderRows", s.model.Order().RowList(), yield, seed)
|
||||||
s.IterateList("NoteRows", s.model.NoteRows(), yield, seed)
|
s.IterateList("NoteRows", s.model.Note().RowList(), yield, seed)
|
||||||
s.IterateList("UnitSearchResults", s.model.SearchResults(), yield, seed)
|
s.IterateList("UnitSearchResults", s.model.Unit().SearchResults(), yield, seed)
|
||||||
s.IterateList("PresetDirs", s.model.PresetDirList().List(), yield, seed)
|
s.IterateList("PresetDirs", s.model.Preset().DirList(), yield, seed)
|
||||||
s.IterateList("PresetResults", s.model.PresetResultList().List(), yield, seed)
|
s.IterateList("PresetResults", s.model.Preset().SearchResultList(), yield, seed)
|
||||||
// Bools
|
// Bools
|
||||||
s.IterateBool("Panic", s.model.Panic(), yield, seed)
|
s.IterateBool("Panic", s.model.Play().Panicked(), yield, seed)
|
||||||
s.IterateBool("Recording", s.model.IsRecording(), yield, seed)
|
s.IterateBool("Recording", s.model.Play().IsRecording(), yield, seed)
|
||||||
s.IterateBool("Playing", s.model.Playing(), yield, seed)
|
s.IterateBool("Playing", s.model.Play().Started(), yield, seed)
|
||||||
s.IterateBool("InstrEnlarged", s.model.InstrEnlarged(), yield, seed)
|
s.IterateBool("InstrEnlarged", s.model.Play().TrackerHidden(), yield, seed)
|
||||||
s.IterateBool("Effect", s.model.Effect(), yield, seed)
|
s.IterateBool("Effect", s.model.Track().Effect(), yield, seed)
|
||||||
s.IterateBool("Follow", s.model.Follow(), yield, seed)
|
s.IterateBool("Follow", s.model.Play().IsFollowing(), yield, seed)
|
||||||
s.IterateBool("UniquePatterns", s.model.UniquePatterns(), yield, seed)
|
s.IterateBool("UniquePatterns", s.model.Note().UniquePatterns(), yield, seed)
|
||||||
s.IterateBool("LinkInstrTrack", s.model.LinkInstrTrack(), yield, seed)
|
s.IterateBool("LinkInstrTrack", s.model.Track().LinkInstrument(), yield, seed)
|
||||||
// Strings
|
// Strings
|
||||||
s.IterateString("FilePath", s.model.FilePath(), yield, seed)
|
s.IterateString("FilePath", s.model.Song().FilePath(), yield, seed)
|
||||||
s.IterateString("InstrumentName", s.model.InstrumentName(), yield, seed)
|
s.IterateString("InstrumentName", s.model.Instrument().Name(), yield, seed)
|
||||||
s.IterateString("InstrumentComment", s.model.InstrumentComment(), yield, seed)
|
s.IterateString("InstrumentComment", s.model.Instrument().Comment(), yield, seed)
|
||||||
s.IterateString("UnitSearchText", s.model.UnitSearch(), yield, seed)
|
s.IterateString("UnitSearchText", s.model.Unit().SearchTerm(), yield, seed)
|
||||||
// Actions
|
// Actions
|
||||||
s.IterateAction("AddTrack", s.model.AddTrack(), yield, seed)
|
s.IterateAction("AddTrack", s.model.Track().Add(), yield, seed)
|
||||||
s.IterateAction("DeleteTrack", s.model.DeleteTrack(), yield, seed)
|
s.IterateAction("DeleteTrack", s.model.Track().Delete(), yield, seed)
|
||||||
s.IterateAction("AddInstrument", s.model.AddInstrument(), yield, seed)
|
s.IterateAction("AddInstrument", s.model.Instrument().Add(), yield, seed)
|
||||||
s.IterateAction("DeleteInstrument", s.model.DeleteInstrument(), yield, seed)
|
s.IterateAction("DeleteInstrument", s.model.Instrument().Delete(), yield, seed)
|
||||||
s.IterateAction("AddUnitAfter", s.model.AddUnit(false), yield, seed)
|
s.IterateAction("AddUnitAfter", s.model.Unit().Add(false), yield, seed)
|
||||||
s.IterateAction("AddUnitBefore", s.model.AddUnit(true), yield, seed)
|
s.IterateAction("AddUnitBefore", s.model.Unit().Add(true), yield, seed)
|
||||||
s.IterateAction("DeleteUnit", s.model.DeleteUnit(), yield, seed)
|
s.IterateAction("DeleteUnit", s.model.Unit().Delete(), yield, seed)
|
||||||
s.IterateAction("ClearUnit", s.model.ClearUnit(), yield, seed)
|
s.IterateAction("ClearUnit", s.model.Unit().Clear(), yield, seed)
|
||||||
s.IterateAction("Undo", s.model.Undo(), yield, seed)
|
s.IterateAction("Undo", s.model.History().Undo(), yield, seed)
|
||||||
s.IterateAction("Redo", s.model.Redo(), yield, seed)
|
s.IterateAction("Redo", s.model.History().Redo(), yield, seed)
|
||||||
s.IterateAction("RemoveUnused", s.model.RemoveUnused(), yield, seed)
|
s.IterateAction("RemoveUnusedPatterns", s.model.Order().RemoveUnusedPatterns(), yield, seed)
|
||||||
s.IterateAction("AddSemitone", s.model.AddSemitone(), yield, seed)
|
s.IterateAction("AddSemitone", s.model.Note().AddSemitone(), yield, seed)
|
||||||
s.IterateAction("SubtractSemitone", s.model.SubtractSemitone(), yield, seed)
|
s.IterateAction("SubtractSemitone", s.model.Note().SubtractSemitone(), yield, seed)
|
||||||
s.IterateAction("AddOctave", s.model.AddOctave(), yield, seed)
|
s.IterateAction("AddOctave", s.model.Note().AddOctave(), yield, seed)
|
||||||
s.IterateAction("SubtractOctave", s.model.SubtractOctave(), yield, seed)
|
s.IterateAction("SubtractOctave", s.model.Note().SubtractOctave(), yield, seed)
|
||||||
s.IterateAction("EditNoteOff", s.model.EditNoteOff(), yield, seed)
|
s.IterateAction("EditNoteOff", s.model.Note().NoteOff(), yield, seed)
|
||||||
s.IterateAction("PlaySongStart", s.model.PlaySongStart(), yield, seed)
|
s.IterateAction("PlaySongStart", s.model.Play().FromBeginning(), yield, seed)
|
||||||
s.IterateAction("AddOrderRowAfter", s.model.AddOrderRow(false), yield, seed)
|
s.IterateAction("AddOrderRowAfter", s.model.Order().AddRow(false), yield, seed)
|
||||||
s.IterateAction("AddOrderRowBefore", s.model.AddOrderRow(true), yield, seed)
|
s.IterateAction("AddOrderRowBefore", s.model.Order().AddRow(true), yield, seed)
|
||||||
s.IterateAction("DeleteOrderRowForward", s.model.DeleteOrderRow(false), yield, seed)
|
s.IterateAction("DeleteOrderRowForward", s.model.Order().DeleteRow(false), yield, seed)
|
||||||
s.IterateAction("DeleteOrderRowBackward", s.model.DeleteOrderRow(true), yield, seed)
|
s.IterateAction("DeleteOrderRowBackward", s.model.Order().DeleteRow(true), yield, seed)
|
||||||
s.IterateAction("SplitInstrument", s.model.SplitInstrument(), yield, seed)
|
s.IterateAction("SplitInstrument", s.model.Instrument().Split(), yield, seed)
|
||||||
s.IterateAction("SplitTrack", s.model.SplitTrack(), yield, seed)
|
s.IterateAction("SplitTrack", s.model.Track().Split(), yield, seed)
|
||||||
// Tables
|
// Tables
|
||||||
s.IterateTable("Order", s.model.Order().Table(), yield, seed)
|
s.IterateTable("Order", s.model.Order().Table(), yield, seed)
|
||||||
s.IterateTable("Notes", s.model.Notes().Table(), yield, seed)
|
s.IterateTable("Notes", s.model.Note().Table(), yield, seed)
|
||||||
// File reading
|
// File reading
|
||||||
if s.file != nil {
|
if s.file != nil {
|
||||||
yield("ReadSong", func(p string, t *testing.T) {
|
yield("ReadSong", func(p string, t *testing.T) {
|
||||||
reader := bytes.NewReader(s.file)
|
reader := bytes.NewReader(s.file)
|
||||||
readCloser := io.NopCloser(reader)
|
readCloser := io.NopCloser(reader)
|
||||||
s.model.ReadSong(readCloser)
|
s.model.Song().Read(readCloser)
|
||||||
})
|
})
|
||||||
yield("LoadInstrument", func(p string, t *testing.T) {
|
yield("LoadInstrument", func(p string, t *testing.T) {
|
||||||
reader := bytes.NewReader(s.file)
|
reader := bytes.NewReader(s.file)
|
||||||
readCloser := io.NopCloser(reader)
|
readCloser := io.NopCloser(reader)
|
||||||
s.model.LoadInstrument(readCloser)
|
s.model.Instrument().Read(readCloser)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// File saving
|
// File saving
|
||||||
yield("WriteSong", func(p string, t *testing.T) {
|
yield("WriteSong", func(p string, t *testing.T) {
|
||||||
writer := bytes.NewBuffer(nil)
|
writer := bytes.NewBuffer(nil)
|
||||||
writeCloser := &myWriteCloser{writer}
|
writeCloser := &myWriteCloser{writer}
|
||||||
s.model.WriteSong(writeCloser)
|
s.model.Song().Write(writeCloser)
|
||||||
s.file = writer.Bytes()
|
s.file = writer.Bytes()
|
||||||
})
|
})
|
||||||
yield("SaveInstrument", func(p string, t *testing.T) {
|
yield("SaveInstrument", func(p string, t *testing.T) {
|
||||||
writer := bytes.NewBuffer(nil)
|
writer := bytes.NewBuffer(nil)
|
||||||
writeCloser := &myWriteCloser{writer}
|
writeCloser := &myWriteCloser{writer}
|
||||||
s.model.SaveInstrument(writeCloser)
|
s.model.Instrument().Write(writeCloser)
|
||||||
s.file = writer.Bytes()
|
s.file = writer.Bytes()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -255,6 +255,7 @@ func FuzzModel(f *testing.F) {
|
|||||||
synthers := []sointu.Synther{vm.GoSynther{}}
|
synthers := []sointu.Synther{vm.GoSynther{}}
|
||||||
broker := tracker.NewBroker()
|
broker := tracker.NewBroker()
|
||||||
model := tracker.NewModel(broker, synthers, tracker.NullMIDIContext{}, "")
|
model := tracker.NewModel(broker, synthers, tracker.NullMIDIContext{}, "")
|
||||||
|
defer model.Close()
|
||||||
player := tracker.NewPlayer(broker, synthers[0])
|
player := tracker.NewPlayer(broker, synthers[0])
|
||||||
buf := make([][2]float32, 2048)
|
buf := make([][2]float32, 2048)
|
||||||
closeChan := make(chan struct{})
|
closeChan := make(chan struct{})
|
||||||
|
|||||||
427
tracker/note.go
Normal file
427
tracker/note.go
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Note returns the Note view of the model, containing methods to manipulate
|
||||||
|
// the note data.
|
||||||
|
func (m *Model) Note() *NoteModel { return (*NoteModel)(m) }
|
||||||
|
|
||||||
|
type NoteModel Model
|
||||||
|
|
||||||
|
// Step returns an Int controlling how many note rows the cursor advances every
|
||||||
|
// time the user inputs a note.
|
||||||
|
func (m *NoteModel) Step() Int { return MakeInt((*noteStep)(m)) }
|
||||||
|
|
||||||
|
type noteStep NoteModel
|
||||||
|
|
||||||
|
func (v *noteStep) Value() int { return v.d.Step }
|
||||||
|
func (v *noteStep) SetValue(value int) bool {
|
||||||
|
defer (*Model)(v).change("StepInt", NoChange, MinorChange)()
|
||||||
|
v.d.Step = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (v *noteStep) Range() RangeInclusive { return RangeInclusive{0, 8} }
|
||||||
|
|
||||||
|
// UniquePatterns returns a Bool controlling whether patterns are made unique
|
||||||
|
// when editing notes.
|
||||||
|
func (m *NoteModel) UniquePatterns() Bool { return MakeBoolFromPtr(&m.uniquePatterns) }
|
||||||
|
|
||||||
|
// Octave returns an Int controlling the current octave for note input.
|
||||||
|
func (m *NoteModel) Octave() Int { return MakeInt((*noteOctave)(m)) }
|
||||||
|
|
||||||
|
type noteOctave NoteModel
|
||||||
|
|
||||||
|
func (v *noteOctave) Value() int { return v.d.Octave }
|
||||||
|
func (v *noteOctave) SetValue(value int) bool { v.d.Octave = value; return true }
|
||||||
|
func (v *noteOctave) Range() RangeInclusive { return RangeInclusive{0, 9} }
|
||||||
|
|
||||||
|
// AddSemiTone returns an Action for adding a semitone to the selected notes.
|
||||||
|
func (m *NoteModel) AddSemitone() Action { return MakeAction((*addSemitone)(m)) }
|
||||||
|
|
||||||
|
type addSemitone NoteModel
|
||||||
|
|
||||||
|
func (m *addSemitone) Do() { Table{(*NoteModel)(m)}.Add(1, false) }
|
||||||
|
|
||||||
|
// SubtractSemitone returns an Action for subtracting a semitone from the
|
||||||
|
// selected notes.
|
||||||
|
func (m *NoteModel) SubtractSemitone() Action { return MakeAction((*subtractSemitone)(m)) }
|
||||||
|
|
||||||
|
type subtractSemitone NoteModel
|
||||||
|
|
||||||
|
func (m *subtractSemitone) Do() { Table{(*NoteModel)(m)}.Add(-1, false) }
|
||||||
|
|
||||||
|
// AddOctave returns an Action for adding an octave to the selected notes.
|
||||||
|
func (m *NoteModel) AddOctave() Action { return MakeAction((*addOctave)(m)) }
|
||||||
|
|
||||||
|
type addOctave NoteModel
|
||||||
|
|
||||||
|
func (m *addOctave) Do() { Table{(*NoteModel)(m)}.Add(1, true) }
|
||||||
|
|
||||||
|
// SubtractOctave returns an Action for subtracting an octave from the selected
|
||||||
|
// notes.
|
||||||
|
func (m *NoteModel) SubtractOctave() Action { return MakeAction((*subtractOctave)(m)) }
|
||||||
|
|
||||||
|
type subtractOctave NoteModel
|
||||||
|
|
||||||
|
func (m *subtractOctave) Do() { Table{(*NoteModel)(m)}.Add(-1, true) }
|
||||||
|
|
||||||
|
// NoteOff returns an Action to set the selected notes to Note Off (0).
|
||||||
|
func (m *NoteModel) NoteOff() Action { return MakeAction((*editNoteOff)(m)) }
|
||||||
|
|
||||||
|
type editNoteOff NoteModel
|
||||||
|
|
||||||
|
func (m *editNoteOff) Do() { Table{(*NoteModel)(m)}.Fill(0) }
|
||||||
|
|
||||||
|
// RowList is a list of all the note rows, implementing ListData & MutableListData
|
||||||
|
// interfaces
|
||||||
|
func (m *NoteModel) RowList() List { return List{(*noteRows)(m)} }
|
||||||
|
|
||||||
|
type noteRows NoteModel
|
||||||
|
|
||||||
|
func (n *noteRows) Count() int { return n.d.Song.Score.Length * n.d.Song.Score.RowsPerPattern }
|
||||||
|
func (n *noteRows) Selected() int { return n.d.Song.Score.SongRow(n.d.Cursor.SongPos) }
|
||||||
|
func (n *noteRows) Selected2() int { return n.d.Song.Score.SongRow(n.d.Cursor2.SongPos) }
|
||||||
|
func (n *noteRows) SetSelected2(v int) { n.d.Cursor2.SongPos = n.d.Song.Score.SongPos(v) }
|
||||||
|
func (n *noteRows) SetSelected(value int) {
|
||||||
|
if value != n.d.Song.Score.SongRow(n.d.Cursor.SongPos) {
|
||||||
|
n.follow = false
|
||||||
|
}
|
||||||
|
n.d.Cursor.SongPos = n.d.Song.Score.Clamp(n.d.Song.Score.SongPos(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *noteRows) Move(r Range, delta int) (ok bool) {
|
||||||
|
for a, b := range r.Swaps(delta) {
|
||||||
|
apos := v.d.Song.Score.SongPos(a)
|
||||||
|
bpos := v.d.Song.Score.SongPos(b)
|
||||||
|
for _, t := range v.d.Song.Score.Tracks {
|
||||||
|
n1 := t.Note(apos)
|
||||||
|
n2 := t.Note(bpos)
|
||||||
|
t.SetNote(apos, n2, v.uniquePatterns)
|
||||||
|
t.SetNote(bpos, n1, v.uniquePatterns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *noteRows) Delete(r Range) (ok bool) {
|
||||||
|
for _, track := range v.d.Song.Score.Tracks {
|
||||||
|
for i := r.Start; i < r.End; i++ {
|
||||||
|
pos := v.d.Song.Score.SongPos(i)
|
||||||
|
track.SetNote(pos, 1, v.uniquePatterns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *noteRows) Change(n string, severity ChangeSeverity) func() {
|
||||||
|
return (*Model)(v).change("NoteRowList."+n, ScoreChange, severity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *noteRows) Cancel() {
|
||||||
|
(*Model)(v).changeCancel = true
|
||||||
|
}
|
||||||
|
|
||||||
|
type marshalNoteRows struct {
|
||||||
|
NoteRows [][]byte `yaml:",flow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *noteRows) Marshal(r Range) ([]byte, error) {
|
||||||
|
var table marshalNoteRows
|
||||||
|
for i, track := range v.d.Song.Score.Tracks {
|
||||||
|
table.NoteRows = append(table.NoteRows, make([]byte, r.Len()))
|
||||||
|
for j := 0; j < r.Len(); j++ {
|
||||||
|
row := r.Start + j
|
||||||
|
pos := v.d.Song.Score.SongPos(row)
|
||||||
|
table.NoteRows[i][j] = track.Note(pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return yaml.Marshal(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *noteRows) Unmarshal(data []byte) (r Range, err error) {
|
||||||
|
var table marshalNoteRows
|
||||||
|
if err := yaml.Unmarshal(data, &table); err != nil {
|
||||||
|
return Range{}, fmt.Errorf("NoteRowList.unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if len(table.NoteRows) < 1 {
|
||||||
|
return Range{}, errors.New("NoteRowList.unmarshal: no tracks")
|
||||||
|
}
|
||||||
|
r.Start = v.d.Song.Score.SongRow(v.d.Cursor.SongPos)
|
||||||
|
for i, arr := range table.NoteRows {
|
||||||
|
if i >= len(v.d.Song.Score.Tracks) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.End = r.Start + len(arr)
|
||||||
|
for j, note := range arr {
|
||||||
|
y := j + r.Start
|
||||||
|
pos := v.d.Song.Score.SongPos(y)
|
||||||
|
v.d.Song.Score.Tracks[i].SetNote(pos, note, v.uniquePatterns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table returns a Table of all the note data.
|
||||||
|
func (v *NoteModel) Table() Table { return Table{v} }
|
||||||
|
|
||||||
|
func (m *NoteModel) Cursor() Point {
|
||||||
|
t := max(min(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||||
|
p := max(min(m.d.Song.Score.SongRow(m.d.Cursor.SongPos), m.d.Song.Score.LengthInRows()-1), 0)
|
||||||
|
return Point{t, p}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *NoteModel) Cursor2() Point {
|
||||||
|
t := max(min(m.d.Cursor2.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||||
|
p := max(min(m.d.Song.Score.SongRow(m.d.Cursor2.SongPos), m.d.Song.Score.LengthInRows()-1), 0)
|
||||||
|
return Point{t, p}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) SetCursor(p Point) {
|
||||||
|
v.d.Cursor.Track = max(min(p.X, len(v.d.Song.Score.Tracks)-1), 0)
|
||||||
|
newPos := v.d.Song.Score.Clamp(sointu.SongPos{PatternRow: p.Y})
|
||||||
|
if newPos != v.d.Cursor.SongPos {
|
||||||
|
v.follow = false
|
||||||
|
}
|
||||||
|
v.d.Cursor.SongPos = newPos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) SetCursor2(p Point) {
|
||||||
|
v.d.Cursor2.Track = max(min(p.X, len(v.d.Song.Score.Tracks)-1), 0)
|
||||||
|
v.d.Cursor2.SongPos = v.d.Song.Score.Clamp(sointu.SongPos{PatternRow: p.Y})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *NoteModel) SetCursorFloat(x, y float32) {
|
||||||
|
m.SetCursor(Point{int(x), int(y)})
|
||||||
|
m.d.LowNibble = math.Mod(float64(x), 1.0) > 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) Width() int {
|
||||||
|
return len((*Model)(v).d.Song.Score.Tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) Height() int {
|
||||||
|
return (*Model)(v).d.Song.Score.Length * (*Model)(v).d.Song.Score.RowsPerPattern
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) MoveCursor(dx, dy int) (ok bool) {
|
||||||
|
p := v.Cursor()
|
||||||
|
for dx < 0 {
|
||||||
|
if (*TrackModel)(v).Item(p.X).Effect && v.d.LowNibble {
|
||||||
|
v.d.LowNibble = false
|
||||||
|
} else {
|
||||||
|
p.X--
|
||||||
|
v.d.LowNibble = true
|
||||||
|
}
|
||||||
|
dx++
|
||||||
|
}
|
||||||
|
for dx > 0 {
|
||||||
|
if (*TrackModel)(v).Item(p.X).Effect && !v.d.LowNibble {
|
||||||
|
v.d.LowNibble = true
|
||||||
|
} else {
|
||||||
|
p.X++
|
||||||
|
v.d.LowNibble = false
|
||||||
|
}
|
||||||
|
dx--
|
||||||
|
}
|
||||||
|
p.Y += dy
|
||||||
|
v.SetCursor(p)
|
||||||
|
return p == v.Cursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) clear(p Point) {
|
||||||
|
v.Input(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) set(p Point, value int) {
|
||||||
|
v.SetValue(p, byte(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) add(rect Rect, delta int, largeStep bool) (ok bool) {
|
||||||
|
if largeStep {
|
||||||
|
delta *= 12
|
||||||
|
}
|
||||||
|
for x := rect.BottomRight.X; x >= rect.TopLeft.X; x-- {
|
||||||
|
for y := rect.BottomRight.Y; y >= rect.TopLeft.Y; y-- {
|
||||||
|
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pos := v.d.Song.Score.SongPos(y)
|
||||||
|
note := v.d.Song.Score.Tracks[x].Note(pos)
|
||||||
|
if note <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newVal := int(note) + delta
|
||||||
|
if newVal < 2 {
|
||||||
|
newVal = 2
|
||||||
|
} else if newVal > 255 {
|
||||||
|
newVal = 255
|
||||||
|
}
|
||||||
|
// only do all sets after all gets, so we don't accidentally adjust single note multiple times
|
||||||
|
defer v.d.Song.Score.Tracks[x].SetNote(pos, byte(newVal), v.uniquePatterns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type noteTable struct {
|
||||||
|
Notes [][]byte `yaml:",flow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *NoteModel) marshal(rect Rect) (data []byte, ok bool) {
|
||||||
|
width := rect.BottomRight.X - rect.TopLeft.X + 1
|
||||||
|
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
|
||||||
|
var table = noteTable{Notes: make([][]byte, 0, width)}
|
||||||
|
for x := 0; x < width; x++ {
|
||||||
|
table.Notes = append(table.Notes, make([]byte, 0, rect.BottomRight.Y-rect.TopLeft.Y+1))
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
pos := m.d.Song.Score.SongPos(y + rect.TopLeft.Y)
|
||||||
|
ax := x + rect.TopLeft.X
|
||||||
|
if ax < 0 || ax >= len(m.d.Song.Score.Tracks) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
table.Notes[x] = append(table.Notes[x], m.d.Song.Score.Tracks[ax].Note(pos))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret, err := yaml.Marshal(table)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return ret, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) unmarshal(data []byte) (noteTable, bool) {
|
||||||
|
var table noteTable
|
||||||
|
yaml.Unmarshal(data, &table)
|
||||||
|
if len(table.Notes) == 0 {
|
||||||
|
return noteTable{}, false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(table.Notes); i++ {
|
||||||
|
if len(table.Notes[i]) > 0 {
|
||||||
|
return table, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return noteTable{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) unmarshalAtCursor(data []byte) bool {
|
||||||
|
table, ok := v.unmarshal(data)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(table.Notes); i++ {
|
||||||
|
for j, q := range table.Notes[i] {
|
||||||
|
x := i + v.Cursor().X
|
||||||
|
y := j + v.Cursor().Y
|
||||||
|
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pos := v.d.Song.Score.SongPos(y)
|
||||||
|
v.d.Song.Score.Tracks[x].SetNote(pos, q, v.uniquePatterns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) unmarshalRange(rect Rect, data []byte) bool {
|
||||||
|
table, ok := v.unmarshal(data)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < rect.Width(); i++ {
|
||||||
|
for j := 0; j < rect.Height(); j++ {
|
||||||
|
k := i % len(table.Notes)
|
||||||
|
l := j % len(table.Notes[k])
|
||||||
|
a := table.Notes[k][l]
|
||||||
|
x := i + rect.TopLeft.X
|
||||||
|
y := j + rect.TopLeft.Y
|
||||||
|
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pos := v.d.Song.Score.SongPos(y)
|
||||||
|
v.d.Song.Score.Tracks[x].SetNote(pos, a, v.uniquePatterns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) change(kind string, severity ChangeSeverity) func() {
|
||||||
|
return (*Model)(v).change("OrderTableView."+kind, ScoreChange, severity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) cancel() {
|
||||||
|
v.changeCancel = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// At returns the note value at the given point.
|
||||||
|
func (m *NoteModel) At(p Point) byte {
|
||||||
|
if p.Y < 0 || p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
pos := m.d.Song.Score.SongPos(p.Y)
|
||||||
|
return m.d.Song.Score.Tracks[p.X].Note(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LowNibble returns whether the user is currently editing the low nibble of the
|
||||||
|
// note value when editing an effect track.
|
||||||
|
func (m *NoteModel) LowNibble() bool { return m.d.LowNibble }
|
||||||
|
|
||||||
|
// SetValue sets the note value at the given point.
|
||||||
|
func (m *NoteModel) SetValue(p Point, val byte) {
|
||||||
|
defer m.change("SetValue", MinorChange)()
|
||||||
|
if p.Y < 0 || p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
track := &(m.d.Song.Score.Tracks[p.X])
|
||||||
|
pos := m.d.Song.Score.SongPos(p.Y)
|
||||||
|
(*track).SetNote(pos, val, m.uniquePatterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input fills the current selection of the note table with a given note value,
|
||||||
|
// returning a NoteEvent telling which note should be played.
|
||||||
|
func (v *NoteModel) Input(note byte) NoteEvent {
|
||||||
|
v.Table().Fill(int(note))
|
||||||
|
return v.finishInput(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputNibble fills the nibbles of current selection of the note table with a
|
||||||
|
// given nibble value. LowNibble tells whether the user is currently editing the
|
||||||
|
// low or high nibbles. It returns a NoteEvent telling which note should be
|
||||||
|
// played.
|
||||||
|
func (v *NoteModel) InputNibble(nibble byte) NoteEvent {
|
||||||
|
defer v.change("FillNibble", MajorChange)()
|
||||||
|
rect := Table{v}.Range()
|
||||||
|
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
||||||
|
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
||||||
|
val := v.At(Point{x, y})
|
||||||
|
if val == 1 {
|
||||||
|
val = 0 // treat hold also as 0
|
||||||
|
}
|
||||||
|
if v.d.LowNibble {
|
||||||
|
val = (val & 0xf0) | byte(nibble&15)
|
||||||
|
} else {
|
||||||
|
val = (val & 0x0f) | byte((nibble&15)<<4)
|
||||||
|
}
|
||||||
|
v.SetValue(Point{x, y}, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v.finishInput(v.At(v.Cursor()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *NoteModel) finishInput(note byte) NoteEvent {
|
||||||
|
if step := v.d.Step; step > 0 {
|
||||||
|
v.Table().MoveCursor(0, step)
|
||||||
|
v.Table().SetCursor2(v.Table().Cursor())
|
||||||
|
}
|
||||||
|
TrySend(v.broker.ToGUI, any(MsgToGUI{Kind: GUIMessageEnsureCursorVisible, Param: v.Table().Cursor().Y}))
|
||||||
|
track := v.Cursor().X
|
||||||
|
ts := time.Now().UnixMilli() * 441 / 10 // convert to 44100Hz frames
|
||||||
|
return NoteEvent{IsTrack: true, Channel: track, Note: note, On: true, Timestamp: ts}
|
||||||
|
}
|
||||||
440
tracker/order.go
Normal file
440
tracker/order.go
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Order returns the Order view of the model, containing methods to manipulate
|
||||||
|
// the pattern order list.
|
||||||
|
func (m *Model) Order() *OrderModel { return (*OrderModel)(m) }
|
||||||
|
|
||||||
|
type OrderModel Model
|
||||||
|
|
||||||
|
// PatternUnique returns true if the given pattern in the given track is used
|
||||||
|
// only once in the pattern order list.
|
||||||
|
func (m *OrderModel) PatternUnique(track, pat int) bool {
|
||||||
|
if track < 0 || track >= len(m.derived.tracks) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if pat < 0 || pat >= len(m.derived.tracks[track].patternUseCounts) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.derived.tracks[track].patternUseCounts[pat] <= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRow returns an Action that adds an order row before or after the current
|
||||||
|
// cursor row.
|
||||||
|
func (m *OrderModel) AddRow(before bool) Action {
|
||||||
|
return MakeAction(addOrderRow{Before: before, Model: (*Model)(m)})
|
||||||
|
}
|
||||||
|
|
||||||
|
type addOrderRow struct {
|
||||||
|
Before bool
|
||||||
|
*Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a addOrderRow) Do() {
|
||||||
|
m := a.Model
|
||||||
|
defer m.change("AddOrderRowAction", ScoreChange, MinorChange)()
|
||||||
|
if !a.Before {
|
||||||
|
m.d.Cursor.OrderRow++
|
||||||
|
}
|
||||||
|
m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow
|
||||||
|
from := m.d.Cursor.OrderRow
|
||||||
|
m.d.Song.Score.Length++
|
||||||
|
for i := range m.d.Song.Score.Tracks {
|
||||||
|
order := &m.d.Song.Score.Tracks[i].Order
|
||||||
|
if len(*order) > from {
|
||||||
|
*order = append(*order, -1)
|
||||||
|
copy((*order)[from+1:], (*order)[from:])
|
||||||
|
(*order)[from] = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRow returns an Action to delete the current row of in the pattern order
|
||||||
|
// list.
|
||||||
|
func (m *OrderModel) DeleteRow(backwards bool) Action {
|
||||||
|
return MakeAction(deleteOrderRow{Backwards: backwards, Model: (*Model)(m)})
|
||||||
|
}
|
||||||
|
|
||||||
|
type deleteOrderRow struct {
|
||||||
|
Backwards bool
|
||||||
|
*Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d deleteOrderRow) Do() {
|
||||||
|
m := d.Model
|
||||||
|
defer m.change("DeleteOrderRowAction", ScoreChange, MinorChange)()
|
||||||
|
from := m.d.Cursor.OrderRow
|
||||||
|
m.d.Song.Score.Length--
|
||||||
|
for i := range m.d.Song.Score.Tracks {
|
||||||
|
order := &m.d.Song.Score.Tracks[i].Order
|
||||||
|
if len(*order) > from {
|
||||||
|
copy((*order)[from:], (*order)[from+1:])
|
||||||
|
*order = (*order)[:len(*order)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.Backwards {
|
||||||
|
if m.d.Cursor.OrderRow > 0 {
|
||||||
|
m.d.Cursor.OrderRow--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table returns a Table of all the pattern order data.
|
||||||
|
func (v *OrderModel) Table() Table { return Table{v} }
|
||||||
|
|
||||||
|
func (m *OrderModel) Cursor() Point {
|
||||||
|
t := max(min(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||||
|
p := max(min(m.d.Cursor.OrderRow, m.d.Song.Score.Length-1), 0)
|
||||||
|
return Point{t, p}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OrderModel) Cursor2() Point {
|
||||||
|
t := max(min(m.d.Cursor2.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||||
|
p := max(min(m.d.Cursor2.OrderRow, m.d.Song.Score.Length-1), 0)
|
||||||
|
return Point{t, p}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OrderModel) SetCursor(p Point) {
|
||||||
|
m.d.Cursor.Track = max(min(p.X, len(m.d.Song.Score.Tracks)-1), 0)
|
||||||
|
y := max(min(p.Y, m.d.Song.Score.Length-1), 0)
|
||||||
|
if y != m.d.Cursor.OrderRow {
|
||||||
|
m.follow = false
|
||||||
|
}
|
||||||
|
m.d.Cursor.OrderRow = y
|
||||||
|
m.updateCursorRows()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OrderModel) SetCursor2(p Point) {
|
||||||
|
m.d.Cursor2.Track = max(min(p.X, len(m.d.Song.Score.Tracks)-1), 0)
|
||||||
|
m.d.Cursor2.OrderRow = max(min(p.Y, m.d.Song.Score.Length-1), 0)
|
||||||
|
m.updateCursorRows()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OrderModel) updateCursorRows() {
|
||||||
|
if v.Cursor() == v.Cursor2() {
|
||||||
|
v.d.Cursor.PatternRow = 0
|
||||||
|
v.d.Cursor2.PatternRow = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if v.d.Cursor.OrderRow > v.d.Cursor2.OrderRow {
|
||||||
|
v.d.Cursor.PatternRow = v.d.Song.Score.RowsPerPattern - 1
|
||||||
|
v.d.Cursor2.PatternRow = 0
|
||||||
|
} else {
|
||||||
|
v.d.Cursor.PatternRow = 0
|
||||||
|
v.d.Cursor2.PatternRow = v.d.Song.Score.RowsPerPattern - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OrderModel) Width() int { return len((*Model)(v).d.Song.Score.Tracks) }
|
||||||
|
func (v *OrderModel) Height() int { return (*Model)(v).d.Song.Score.Length }
|
||||||
|
|
||||||
|
func (v *OrderModel) MoveCursor(dx, dy int) (ok bool) {
|
||||||
|
p := v.Cursor()
|
||||||
|
p.X += dx
|
||||||
|
p.Y += dy
|
||||||
|
v.SetCursor(p)
|
||||||
|
return p == v.Cursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OrderModel) clear(p Point) {
|
||||||
|
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OrderModel) set(p Point, value int) {
|
||||||
|
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OrderModel) add(rect Rect, delta int, largeStep bool) (ok bool) {
|
||||||
|
if largeStep {
|
||||||
|
delta *= 8
|
||||||
|
}
|
||||||
|
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
||||||
|
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
||||||
|
if !v.add1(Point{x, y}, delta) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OrderModel) add1(p Point, delta int) (ok bool) {
|
||||||
|
if p.X < 0 || p.X >= len(v.d.Song.Score.Tracks) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val := v.d.Song.Score.Tracks[p.X].Order.Get(p.Y)
|
||||||
|
if val < 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val += delta
|
||||||
|
if val < 0 || val > 36 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type marshalOrder struct {
|
||||||
|
Order []int `yaml:",flow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type marshalTracks struct {
|
||||||
|
Tracks []marshalOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OrderModel) marshal(rect Rect) (data []byte, ok bool) {
|
||||||
|
width := rect.BottomRight.X - rect.TopLeft.X + 1
|
||||||
|
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
|
||||||
|
var table = marshalTracks{Tracks: make([]marshalOrder, 0, width)}
|
||||||
|
for x := 0; x < width; x++ {
|
||||||
|
ax := x + rect.TopLeft.X
|
||||||
|
if ax < 0 || ax >= len(m.d.Song.Score.Tracks) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
table.Tracks = append(table.Tracks, marshalOrder{Order: make([]int, 0, rect.BottomRight.Y-rect.TopLeft.Y+1)})
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
table.Tracks[x].Order = append(table.Tracks[x].Order, m.d.Song.Score.Tracks[ax].Order.Get(y+rect.TopLeft.Y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret, err := yaml.Marshal(table)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return ret, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OrderModel) unmarshal(data []byte) (marshalTracks, bool) {
|
||||||
|
var table marshalTracks
|
||||||
|
yaml.Unmarshal(data, &table)
|
||||||
|
if len(table.Tracks) == 0 {
|
||||||
|
return marshalTracks{}, false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(table.Tracks); i++ {
|
||||||
|
if len(table.Tracks[i].Order) > 0 {
|
||||||
|
return table, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return marshalTracks{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OrderModel) unmarshalAtCursor(data []byte) bool {
|
||||||
|
table, ok := v.unmarshal(data)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(table.Tracks); i++ {
|
||||||
|
for j, q := range table.Tracks[i].Order {
|
||||||
|
if table.Tracks[i].Order[j] < -1 || table.Tracks[i].Order[j] > 36 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
x := i + v.Cursor().X
|
||||||
|
y := j + v.Cursor().Y
|
||||||
|
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.Length {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v.d.Song.Score.Tracks[x].Order.Set(y, q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OrderModel) unmarshalRange(rect Rect, data []byte) bool {
|
||||||
|
table, ok := v.unmarshal(data)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < rect.Width(); i++ {
|
||||||
|
for j := 0; j < rect.Height(); j++ {
|
||||||
|
k := i % len(table.Tracks)
|
||||||
|
l := j % len(table.Tracks[k].Order)
|
||||||
|
a := table.Tracks[k].Order[l]
|
||||||
|
if a < -1 || a > 36 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
x := i + rect.TopLeft.X
|
||||||
|
y := j + rect.TopLeft.Y
|
||||||
|
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.Length {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v.d.Song.Score.Tracks[x].Order.Set(y, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OrderModel) change(kind string, severity ChangeSeverity) func() {
|
||||||
|
return (*Model)(v).change("OrderTableView."+kind, ScoreChange, severity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *OrderModel) cancel() {
|
||||||
|
v.changeCancel = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OrderModel) Value(p Point) int {
|
||||||
|
if p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return m.d.Song.Score.Tracks[p.X].Order.Get(p.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *OrderModel) SetValue(p Point, val int) {
|
||||||
|
defer (*Model)(m).change("OrderElement.SetValue", ScoreChange, MinorChange)()
|
||||||
|
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RowList returns a List of all the rows of the pattern order table.
|
||||||
|
func (m *OrderModel) RowList() List { return List{(*orderRows)(m)} }
|
||||||
|
|
||||||
|
type orderRows OrderModel
|
||||||
|
|
||||||
|
func (v *orderRows) Count() int { return v.d.Song.Score.Length }
|
||||||
|
func (v *orderRows) Selected() int { return v.d.Cursor.OrderRow }
|
||||||
|
func (v *orderRows) Selected2() int { return v.d.Cursor2.OrderRow }
|
||||||
|
func (v *orderRows) SetSelected2(value int) { v.d.Cursor2.OrderRow = value }
|
||||||
|
func (v *orderRows) SetSelected(value int) {
|
||||||
|
if value != v.d.Cursor.OrderRow {
|
||||||
|
v.follow = false
|
||||||
|
}
|
||||||
|
v.d.Cursor.OrderRow = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *orderRows) Move(r Range, delta int) (ok bool) {
|
||||||
|
swaps := r.Swaps(delta)
|
||||||
|
for i, t := range v.d.Song.Score.Tracks {
|
||||||
|
for a, b := range swaps {
|
||||||
|
ea, eb := t.Order.Get(a), t.Order.Get(b)
|
||||||
|
v.d.Song.Score.Tracks[i].Order.Set(a, eb)
|
||||||
|
v.d.Song.Score.Tracks[i].Order.Set(b, ea)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *orderRows) Delete(r Range) (ok bool) {
|
||||||
|
for i, t := range v.d.Song.Score.Tracks {
|
||||||
|
r2 := r.Intersect(Range{0, len(t.Order)})
|
||||||
|
v.d.Song.Score.Tracks[i].Order = append(t.Order[:r2.Start], t.Order[r2.End:]...)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *orderRows) Change(n string, severity ChangeSeverity) func() {
|
||||||
|
return (*Model)(v).change("OrderRowList."+n, ScoreChange, severity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *orderRows) Cancel() {
|
||||||
|
v.changeCancel = true
|
||||||
|
}
|
||||||
|
|
||||||
|
type marshalOrderRows struct {
|
||||||
|
Columns [][]int `yaml:",flow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *orderRows) Marshal(r Range) ([]byte, error) {
|
||||||
|
var table marshalOrderRows
|
||||||
|
for i := range v.d.Song.Score.Tracks {
|
||||||
|
table.Columns = append(table.Columns, make([]int, r.Len()))
|
||||||
|
for j := 0; j < r.Len(); j++ {
|
||||||
|
table.Columns[i][j] = v.d.Song.Score.Tracks[i].Order.Get(r.Start + j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return yaml.Marshal(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *orderRows) Unmarshal(data []byte) (r Range, err error) {
|
||||||
|
var table marshalOrderRows
|
||||||
|
err = yaml.Unmarshal(data, &table)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(table.Columns) == 0 {
|
||||||
|
err = errors.New("OrderRowList.unmarshal: no rows")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Start = v.d.Cursor.OrderRow
|
||||||
|
r.End = v.d.Cursor.OrderRow + len(table.Columns[0])
|
||||||
|
for i := range v.d.Song.Score.Tracks {
|
||||||
|
if i >= len(table.Columns) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
order := &v.d.Song.Score.Tracks[i].Order
|
||||||
|
for j := 0; j < r.Start-len(*order); j++ {
|
||||||
|
*order = append(*order, -1)
|
||||||
|
}
|
||||||
|
if len(*order) > r.Start {
|
||||||
|
table.Columns[i] = append(table.Columns[i], (*order)[r.Start:]...)
|
||||||
|
*order = (*order)[:r.Start]
|
||||||
|
}
|
||||||
|
*order = append(*order, table.Columns[i]...)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveUnused returns an Action that removes all unused patterns from all
|
||||||
|
// tracks in the song, and updates the pattern orders accordingly.
|
||||||
|
func (m *OrderModel) RemoveUnusedPatterns() Action { return MakeAction((*removeUnused)(m)) }
|
||||||
|
|
||||||
|
type removeUnused OrderModel
|
||||||
|
|
||||||
|
func (m *removeUnused) Do() {
|
||||||
|
defer (*Model)(m).change("RemoveUnusedAction", ScoreChange, MajorChange)()
|
||||||
|
for trkIndex, trk := range m.d.Song.Score.Tracks {
|
||||||
|
// assign new indices to patterns
|
||||||
|
newIndex := map[int]int{}
|
||||||
|
runningIndex := 0
|
||||||
|
length := 0
|
||||||
|
if len(trk.Order) > m.d.Song.Score.Length {
|
||||||
|
trk.Order = trk.Order[:m.d.Song.Score.Length]
|
||||||
|
}
|
||||||
|
for i, p := range trk.Order {
|
||||||
|
// if the pattern hasn't been considered and is within limits
|
||||||
|
if _, ok := newIndex[p]; !ok && p >= 0 && p < len(trk.Patterns) {
|
||||||
|
pat := trk.Patterns[p]
|
||||||
|
useful := false
|
||||||
|
for _, n := range pat { // patterns that have anything else than all holds are useful and to be kept
|
||||||
|
if n != 1 {
|
||||||
|
useful = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if useful {
|
||||||
|
newIndex[p] = runningIndex
|
||||||
|
runningIndex++
|
||||||
|
} else {
|
||||||
|
newIndex[p] = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ind, ok := newIndex[p]; ok && ind > -1 {
|
||||||
|
length = i + 1
|
||||||
|
trk.Order[i] = ind
|
||||||
|
} else {
|
||||||
|
trk.Order[i] = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trk.Order = trk.Order[:length]
|
||||||
|
newPatterns := make([]sointu.Pattern, runningIndex)
|
||||||
|
for i, pat := range trk.Patterns {
|
||||||
|
if ind, ok := newIndex[i]; ok && ind > -1 {
|
||||||
|
patLength := 0
|
||||||
|
for j, note := range pat { // find last note that is something else that hold
|
||||||
|
if note != 1 {
|
||||||
|
patLength = j + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if patLength > m.d.Song.Score.RowsPerPattern {
|
||||||
|
patLength = m.d.Song.Score.RowsPerPattern
|
||||||
|
}
|
||||||
|
newPatterns[ind] = pat[:patLength] // crop to either RowsPerPattern or last row having something else than hold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trk.Patterns = newPatterns
|
||||||
|
m.d.Song.Score.Tracks[trkIndex] = trk
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,228 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Params returns the Param view of the Model, containing methods to manipulate
|
||||||
|
// the parameters.
|
||||||
|
func (m *Model) Params() *ParamModel { return (*ParamModel)(m) }
|
||||||
|
|
||||||
|
type ParamModel Model
|
||||||
|
|
||||||
|
// Wires returns the wires of the current instrument, telling which parameters
|
||||||
|
// are connected to which.
|
||||||
|
func (m *ParamModel) Wires(yield func(wire Wire) bool) {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.derived.patch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, wire := range m.derived.patch[i].wires {
|
||||||
|
wire.Highlight = (wire.FromSet && m.d.UnitIndex == wire.From) || (wire.ToSet && m.d.UnitIndex == wire.To.Y && m.d.ParamIndex == wire.To.X)
|
||||||
|
if !yield(wire) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// chooseSendSource
|
||||||
|
type chooseSendSource struct {
|
||||||
|
ID int
|
||||||
|
*Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ParamModel) IsChoosingSendTarget() bool {
|
||||||
|
return m.d.SendSource > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ParamModel) ChooseSendSource(id int) Action {
|
||||||
|
return MakeAction(chooseSendSource{ID: id, Model: (*Model)(m)})
|
||||||
|
}
|
||||||
|
func (s chooseSendSource) Do() {
|
||||||
|
defer (*Model)(s.Model).change("ChooseSendSource", NoChange, MinorChange)()
|
||||||
|
if s.Model.d.SendSource == s.ID {
|
||||||
|
s.Model.d.SendSource = 0 // unselect
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Model.d.SendSource = s.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// chooseSendTarget
|
||||||
|
type chooseSendTarget struct {
|
||||||
|
ID int
|
||||||
|
Port int
|
||||||
|
*Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ParamModel) ChooseSendTarget(id int, port int) Action {
|
||||||
|
return MakeAction(chooseSendTarget{ID: id, Port: port, Model: (*Model)(m)})
|
||||||
|
}
|
||||||
|
func (s chooseSendTarget) Do() {
|
||||||
|
defer (*Model)(s.Model).change("ChooseSendTarget", SongChange, MinorChange)()
|
||||||
|
sourceID := (*Model)(s.Model).d.SendSource
|
||||||
|
s.d.SendSource = 0
|
||||||
|
if sourceID <= 0 || s.ID <= 0 || s.Port < 0 || s.Port > 7 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
si, su, err := s.d.Song.Patch.FindUnit(sourceID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.d.Song.Patch[si].Units[su].Parameters["target"] = s.ID
|
||||||
|
s.d.Song.Patch[si].Units[su].Parameters["port"] = s.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
// paramsColumns
|
||||||
|
type paramsColumns Model
|
||||||
|
|
||||||
|
func (m *ParamModel) Columns() List { return List{(*paramsColumns)(m)} }
|
||||||
|
func (pt *paramsColumns) Selected() int { return pt.d.ParamIndex }
|
||||||
|
func (pt *paramsColumns) Selected2() int { return pt.d.ParamIndex }
|
||||||
|
func (pt *paramsColumns) SetSelected(index int) { pt.d.ParamIndex = index }
|
||||||
|
func (pt *paramsColumns) SetSelected2(index int) {}
|
||||||
|
func (pt *paramsColumns) Count() int { return (*ParamModel)(pt).Width() }
|
||||||
|
|
||||||
|
// Model and Params methods
|
||||||
|
|
||||||
|
func (pt *ParamModel) Table() Table { return Table{pt} }
|
||||||
|
func (pt *ParamModel) Cursor() Point { return Point{pt.d.ParamIndex, pt.d.UnitIndex} }
|
||||||
|
func (pt *ParamModel) Cursor2() Point { return Point{pt.d.ParamIndex, pt.d.UnitIndex2} }
|
||||||
|
func (pt *ParamModel) SetCursor(p Point) {
|
||||||
|
pt.d.ParamIndex = max(min(p.X, pt.Width()-1), 0)
|
||||||
|
pt.d.UnitIndex = max(min(p.Y, pt.Height()-1), 0)
|
||||||
|
}
|
||||||
|
func (pt *ParamModel) SetCursor2(p Point) {
|
||||||
|
pt.d.ParamIndex = max(min(p.X, pt.Width()-1), 0)
|
||||||
|
pt.d.UnitIndex2 = max(min(p.Y, pt.Height()-1), 0)
|
||||||
|
}
|
||||||
|
func (pt *ParamModel) Width() int {
|
||||||
|
if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.derived.patch) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// TODO: we hack the +1 so that we always have one extra cell to draw the
|
||||||
|
// comments. Refactor the gioui side so that we can specify the width and
|
||||||
|
// height regardless of the underlying table size
|
||||||
|
return pt.derived.patch[pt.d.InstrIndex].paramsWidth + 1
|
||||||
|
}
|
||||||
|
func (pt *ParamModel) RowWidth(y int) int {
|
||||||
|
if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.derived.patch) || y < 0 || y >= len(pt.derived.patch[pt.d.InstrIndex].params) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(pt.derived.patch[pt.d.InstrIndex].params[y])
|
||||||
|
}
|
||||||
|
func (pt *ParamModel) Height() int { return (*Model)(pt).Unit().List().Count() }
|
||||||
|
func (pt *ParamModel) MoveCursor(dx, dy int) (ok bool) {
|
||||||
|
p := pt.Cursor()
|
||||||
|
p.X += dx
|
||||||
|
p.Y += dy
|
||||||
|
pt.SetCursor(p)
|
||||||
|
return p == pt.Cursor()
|
||||||
|
}
|
||||||
|
func (pt *ParamModel) Item(p Point) Parameter {
|
||||||
|
if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.derived.patch) || p.Y < 0 || p.Y >= len(pt.derived.patch[pt.d.InstrIndex].params) || p.X < 0 || p.X >= len(pt.derived.patch[pt.d.InstrIndex].params[p.Y]) {
|
||||||
|
return Parameter{}
|
||||||
|
}
|
||||||
|
return pt.derived.patch[pt.d.InstrIndex].params[p.Y][p.X]
|
||||||
|
}
|
||||||
|
func (pt *ParamModel) clear(p Point) {
|
||||||
|
q := pt.Item(p)
|
||||||
|
q.Reset()
|
||||||
|
}
|
||||||
|
func (pt *ParamModel) set(p Point, value int) {
|
||||||
|
q := pt.Item(p)
|
||||||
|
q.SetValue(value)
|
||||||
|
}
|
||||||
|
func (pt *ParamModel) add(rect Rect, delta int, largeStep bool) (ok bool) {
|
||||||
|
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
||||||
|
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
||||||
|
p := Point{x, y}
|
||||||
|
q := pt.Item(p)
|
||||||
|
if !q.Add(delta, largeStep) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type paramsTable struct {
|
||||||
|
Params [][]int `yaml:",flow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt *ParamModel) marshal(rect Rect) (data []byte, ok bool) {
|
||||||
|
width := rect.BottomRight.X - rect.TopLeft.X + 1
|
||||||
|
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
|
||||||
|
var table = paramsTable{Params: make([][]int, 0, width)}
|
||||||
|
for x := 0; x < width; x++ {
|
||||||
|
table.Params = append(table.Params, make([]int, 0, rect.BottomRight.Y-rect.TopLeft.Y+1))
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
p := pt.Item(Point{x + rect.TopLeft.X, y + rect.TopLeft.Y})
|
||||||
|
table.Params[x] = append(table.Params[x], p.Value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret, err := yaml.Marshal(table)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return ret, true
|
||||||
|
}
|
||||||
|
func (pt *ParamModel) unmarshal(data []byte) (paramsTable, bool) {
|
||||||
|
var table paramsTable
|
||||||
|
yaml.Unmarshal(data, &table)
|
||||||
|
if len(table.Params) == 0 {
|
||||||
|
return paramsTable{}, false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(table.Params); i++ {
|
||||||
|
if len(table.Params[i]) > 0 {
|
||||||
|
return table, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paramsTable{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt *ParamModel) unmarshalAtCursor(data []byte) (ret bool) {
|
||||||
|
table, ok := pt.unmarshal(data)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(table.Params); i++ {
|
||||||
|
for j, q := range table.Params[i] {
|
||||||
|
x := i + pt.Cursor().X
|
||||||
|
y := j + pt.Cursor().Y
|
||||||
|
p := pt.Item(Point{x, y})
|
||||||
|
ret = p.SetValue(q) || ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
func (pt *ParamModel) unmarshalRange(rect Rect, data []byte) (ret bool) {
|
||||||
|
table, ok := pt.unmarshal(data)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(table.Params) == 0 || len(table.Params[0]) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
width := rect.BottomRight.X - rect.TopLeft.X + 1
|
||||||
|
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
|
||||||
|
if len(table.Params) < width {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for x := 0; x < width; x++ {
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
if len(table.Params[0]) < height {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
p := pt.Item(Point{x + rect.TopLeft.X, y + rect.TopLeft.Y})
|
||||||
|
ret = p.SetValue(table.Params[x][y]) || ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
func (pt *ParamModel) change(kind string, severity ChangeSeverity) func() {
|
||||||
|
return (*Model)(pt).change(kind, PatchChange, severity)
|
||||||
|
}
|
||||||
|
func (pt *ParamModel) cancel() {
|
||||||
|
pt.changeCancel = true
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Parameter represents a parameter of a unit. To support polymorphism
|
// Parameter represents a parameter of a unit. To support polymorphism
|
||||||
// without causing allocations, it has a vtable that defines the methods for
|
// without causing allocations, it has a vtable that defines the methods for
|
||||||
@@ -27,7 +249,7 @@ type (
|
|||||||
parameterVtable interface {
|
parameterVtable interface {
|
||||||
Value(*Parameter) int
|
Value(*Parameter) int
|
||||||
SetValue(*Parameter, int) bool
|
SetValue(*Parameter, int) bool
|
||||||
Range(*Parameter) IntRange
|
Range(*Parameter) RangeInclusive
|
||||||
Type(*Parameter) ParameterType
|
Type(*Parameter) ParameterType
|
||||||
Name(*Parameter) string
|
Name(*Parameter) string
|
||||||
Hint(*Parameter) ParameterHint
|
Hint(*Parameter) ParameterHint
|
||||||
@@ -35,9 +257,6 @@ type (
|
|||||||
RoundToGrid(*Parameter, int, bool) int
|
RoundToGrid(*Parameter, int, bool) int
|
||||||
}
|
}
|
||||||
|
|
||||||
Params Model
|
|
||||||
ParamVertList Model
|
|
||||||
|
|
||||||
// different parameter vtables to handle different types of parameters.
|
// different parameter vtables to handle different types of parameters.
|
||||||
// Casting struct{} to interface does not cause allocations.
|
// Casting struct{} to interface does not cause allocations.
|
||||||
namedParameter struct{}
|
namedParameter struct{}
|
||||||
@@ -99,9 +318,9 @@ func (p *Parameter) Add(delta int, snapToGrid bool) bool {
|
|||||||
return p.SetValue(newVal)
|
return p.SetValue(newVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Parameter) Range() IntRange {
|
func (p *Parameter) Range() RangeInclusive {
|
||||||
if p.vtable == nil {
|
if p.vtable == nil {
|
||||||
return IntRange{}
|
return RangeInclusive{}
|
||||||
}
|
}
|
||||||
return p.vtable.Range(p)
|
return p.vtable.Range(p)
|
||||||
}
|
}
|
||||||
@@ -145,161 +364,6 @@ func (p *Parameter) UnitID() int {
|
|||||||
return p.unit.ID
|
return p.unit.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
func (m *Model) ParamVertList() *ParamVertList { return (*ParamVertList)(m) }
|
|
||||||
func (pt *ParamVertList) List() List { return List{pt} }
|
|
||||||
func (pt *ParamVertList) Selected() int { return pt.d.ParamIndex }
|
|
||||||
func (pt *ParamVertList) Selected2() int { return pt.d.ParamIndex }
|
|
||||||
func (pt *ParamVertList) SetSelected(index int) { pt.d.ParamIndex = index }
|
|
||||||
func (pt *ParamVertList) SetSelected2(index int) {}
|
|
||||||
func (pt *ParamVertList) Count() int { return (*Params)(pt).Width() }
|
|
||||||
|
|
||||||
// Model and Params methods
|
|
||||||
|
|
||||||
func (m *Model) Params() *Params { return (*Params)(m) }
|
|
||||||
func (pt *Params) Table() Table { return Table{pt} }
|
|
||||||
func (pt *Params) Cursor() Point { return Point{pt.d.ParamIndex, pt.d.UnitIndex} }
|
|
||||||
func (pt *Params) Cursor2() Point { return Point{pt.d.ParamIndex, pt.d.UnitIndex2} }
|
|
||||||
func (pt *Params) SetCursor(p Point) {
|
|
||||||
pt.d.ParamIndex = max(min(p.X, pt.Width()-1), 0)
|
|
||||||
pt.d.UnitIndex = max(min(p.Y, pt.Height()-1), 0)
|
|
||||||
}
|
|
||||||
func (pt *Params) SetCursor2(p Point) {
|
|
||||||
pt.d.ParamIndex = max(min(p.X, pt.Width()-1), 0)
|
|
||||||
pt.d.UnitIndex2 = max(min(p.Y, pt.Height()-1), 0)
|
|
||||||
}
|
|
||||||
func (pt *Params) Width() int {
|
|
||||||
if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.derived.patch) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
// TODO: we hack the +1 so that we always have one extra cell to draw the
|
|
||||||
// comments. Refactor the gioui side so that we can specify the width and
|
|
||||||
// height regardless of the underlying table size
|
|
||||||
return pt.derived.patch[pt.d.InstrIndex].paramsWidth + 1
|
|
||||||
}
|
|
||||||
func (pt *Params) RowWidth(y int) int {
|
|
||||||
if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.derived.patch) || y < 0 || y >= len(pt.derived.patch[pt.d.InstrIndex].params) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return len(pt.derived.patch[pt.d.InstrIndex].params[y])
|
|
||||||
}
|
|
||||||
func (pt *Params) Height() int { return (*Model)(pt).Units().Count() }
|
|
||||||
func (pt *Params) MoveCursor(dx, dy int) (ok bool) {
|
|
||||||
p := pt.Cursor()
|
|
||||||
p.X += dx
|
|
||||||
p.Y += dy
|
|
||||||
pt.SetCursor(p)
|
|
||||||
return p == pt.Cursor()
|
|
||||||
}
|
|
||||||
func (pt *Params) Item(p Point) Parameter {
|
|
||||||
if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.derived.patch) || p.Y < 0 || p.Y >= len(pt.derived.patch[pt.d.InstrIndex].params) || p.X < 0 || p.X >= len(pt.derived.patch[pt.d.InstrIndex].params[p.Y]) {
|
|
||||||
return Parameter{}
|
|
||||||
}
|
|
||||||
return pt.derived.patch[pt.d.InstrIndex].params[p.Y][p.X]
|
|
||||||
}
|
|
||||||
func (pt *Params) clear(p Point) {
|
|
||||||
q := pt.Item(p)
|
|
||||||
q.Reset()
|
|
||||||
}
|
|
||||||
func (pt *Params) set(p Point, value int) {
|
|
||||||
q := pt.Item(p)
|
|
||||||
q.SetValue(value)
|
|
||||||
}
|
|
||||||
func (pt *Params) add(rect Rect, delta int, largeStep bool) (ok bool) {
|
|
||||||
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
|
||||||
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
|
||||||
p := Point{x, y}
|
|
||||||
q := pt.Item(p)
|
|
||||||
if !q.Add(delta, largeStep) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type paramsTable struct {
|
|
||||||
Params [][]int `yaml:",flow"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pt *Params) marshal(rect Rect) (data []byte, ok bool) {
|
|
||||||
width := rect.BottomRight.X - rect.TopLeft.X + 1
|
|
||||||
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
|
|
||||||
var table = paramsTable{Params: make([][]int, 0, width)}
|
|
||||||
for x := 0; x < width; x++ {
|
|
||||||
table.Params = append(table.Params, make([]int, 0, rect.BottomRight.Y-rect.TopLeft.Y+1))
|
|
||||||
for y := 0; y < height; y++ {
|
|
||||||
p := pt.Item(Point{x + rect.TopLeft.X, y + rect.TopLeft.Y})
|
|
||||||
table.Params[x] = append(table.Params[x], p.Value())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret, err := yaml.Marshal(table)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return ret, true
|
|
||||||
}
|
|
||||||
func (pt *Params) unmarshal(data []byte) (paramsTable, bool) {
|
|
||||||
var table paramsTable
|
|
||||||
yaml.Unmarshal(data, &table)
|
|
||||||
if len(table.Params) == 0 {
|
|
||||||
return paramsTable{}, false
|
|
||||||
}
|
|
||||||
for i := 0; i < len(table.Params); i++ {
|
|
||||||
if len(table.Params[i]) > 0 {
|
|
||||||
return table, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return paramsTable{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pt *Params) unmarshalAtCursor(data []byte) (ret bool) {
|
|
||||||
table, ok := pt.unmarshal(data)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := 0; i < len(table.Params); i++ {
|
|
||||||
for j, q := range table.Params[i] {
|
|
||||||
x := i + pt.Cursor().X
|
|
||||||
y := j + pt.Cursor().Y
|
|
||||||
p := pt.Item(Point{x, y})
|
|
||||||
ret = p.SetValue(q) || ret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
func (pt *Params) unmarshalRange(rect Rect, data []byte) (ret bool) {
|
|
||||||
table, ok := pt.unmarshal(data)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(table.Params) == 0 || len(table.Params[0]) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
width := rect.BottomRight.X - rect.TopLeft.X + 1
|
|
||||||
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
|
|
||||||
if len(table.Params) < width {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for x := 0; x < width; x++ {
|
|
||||||
for y := 0; y < height; y++ {
|
|
||||||
if len(table.Params[0]) < height {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
p := pt.Item(Point{x + rect.TopLeft.X, y + rect.TopLeft.Y})
|
|
||||||
ret = p.SetValue(table.Params[x][y]) || ret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
func (pt *Params) change(kind string, severity ChangeSeverity) func() {
|
|
||||||
return (*Model)(pt).change(kind, PatchChange, severity)
|
|
||||||
}
|
|
||||||
func (pt *Params) cancel() {
|
|
||||||
pt.changeCancel = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// namedParameter vtable
|
// namedParameter vtable
|
||||||
|
|
||||||
func (n *namedParameter) Value(p *Parameter) int { return p.unit.Parameters[p.up.Name] }
|
func (n *namedParameter) Value(p *Parameter) int { return p.unit.Parameters[p.up.Name] }
|
||||||
@@ -308,8 +372,8 @@ func (n *namedParameter) SetValue(p *Parameter, value int) bool {
|
|||||||
p.unit.Parameters[p.up.Name] = value
|
p.unit.Parameters[p.up.Name] = value
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
func (n *namedParameter) Range(p *Parameter) IntRange {
|
func (n *namedParameter) Range(p *Parameter) RangeInclusive {
|
||||||
return IntRange{Min: p.up.MinValue, Max: p.up.MaxValue}
|
return RangeInclusive{Min: p.up.MinValue, Max: p.up.MaxValue}
|
||||||
}
|
}
|
||||||
func (n *namedParameter) Type(p *Parameter) ParameterType {
|
func (n *namedParameter) Type(p *Parameter) ParameterType {
|
||||||
if p.up == nil || !p.up.CanSet {
|
if p.up == nil || !p.up.CanSet {
|
||||||
@@ -353,6 +417,25 @@ func (n *namedParameter) Reset(p *Parameter) {
|
|||||||
p.unit.Parameters[p.up.Name] = v
|
p.unit.Parameters[p.up.Name] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GmDlsEntry is a single sample entry from the gm.dls file
|
||||||
|
type GmDlsEntry struct {
|
||||||
|
Start int // sample start offset in words
|
||||||
|
LoopStart int // loop start offset in words
|
||||||
|
LoopLength int // loop length in words
|
||||||
|
SuggestedTranspose int // suggested transpose in semitones, so that all samples play at same pitch
|
||||||
|
Name string // sample Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// gmDlsEntryMap is a reverse map, to find the index of the GmDlsEntry in the
|
||||||
|
var gmDlsEntryMap = make(map[vm.SampleOffset]int)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for i, e := range GmDlsEntries {
|
||||||
|
key := vm.SampleOffset{Start: uint32(e.Start), LoopStart: uint16(e.LoopStart), LoopLength: uint16(e.LoopLength)}
|
||||||
|
gmDlsEntryMap[key] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// gmDlsEntryParameter vtable
|
// gmDlsEntryParameter vtable
|
||||||
|
|
||||||
func (g *gmDlsEntryParameter) Value(p *Parameter) int {
|
func (g *gmDlsEntryParameter) Value(p *Parameter) int {
|
||||||
@@ -378,8 +461,8 @@ func (g *gmDlsEntryParameter) SetValue(p *Parameter, v int) bool {
|
|||||||
p.unit.Parameters["transpose"] = 64 + e.SuggestedTranspose
|
p.unit.Parameters["transpose"] = 64 + e.SuggestedTranspose
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
func (g *gmDlsEntryParameter) Range(p *Parameter) IntRange {
|
func (g *gmDlsEntryParameter) Range(p *Parameter) RangeInclusive {
|
||||||
return IntRange{Min: 0, Max: len(GmDlsEntries)}
|
return RangeInclusive{Min: 0, Max: len(GmDlsEntries)}
|
||||||
}
|
}
|
||||||
func (g *gmDlsEntryParameter) Type(p *Parameter) ParameterType {
|
func (g *gmDlsEntryParameter) Type(p *Parameter) ParameterType {
|
||||||
return IntegerParameter
|
return IntegerParameter
|
||||||
@@ -429,11 +512,11 @@ func (d *delayTimeParameter) SetValue(p *Parameter, v int) bool {
|
|||||||
p.unit.VarArgs[p.index] = v
|
p.unit.VarArgs[p.index] = v
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
func (d *delayTimeParameter) Range(p *Parameter) IntRange {
|
func (d *delayTimeParameter) Range(p *Parameter) RangeInclusive {
|
||||||
if p.unit.Parameters["notetracking"] == 2 {
|
if p.unit.Parameters["notetracking"] == 2 {
|
||||||
return IntRange{Min: 1, Max: 576}
|
return RangeInclusive{Min: 1, Max: 576}
|
||||||
}
|
}
|
||||||
return IntRange{Min: 1, Max: 65535}
|
return RangeInclusive{Min: 1, Max: 65535}
|
||||||
}
|
}
|
||||||
func (d *delayTimeParameter) Hint(p *Parameter) ParameterHint {
|
func (d *delayTimeParameter) Hint(p *Parameter) ParameterHint {
|
||||||
val := d.Value(p)
|
val := d.Value(p)
|
||||||
@@ -511,7 +594,9 @@ func (d *delayLinesParameter) SetValue(p *Parameter, v int) bool {
|
|||||||
p.unit.VarArgs = p.unit.VarArgs[:targetLines]
|
p.unit.VarArgs = p.unit.VarArgs[:targetLines]
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
func (d *delayLinesParameter) Range(p *Parameter) IntRange { return IntRange{Min: 1, Max: 32} }
|
func (d *delayLinesParameter) Range(p *Parameter) RangeInclusive {
|
||||||
|
return RangeInclusive{Min: 1, Max: 32}
|
||||||
|
}
|
||||||
func (d *delayLinesParameter) Type(p *Parameter) ParameterType { return IntegerParameter }
|
func (d *delayLinesParameter) Type(p *Parameter) ParameterType { return IntegerParameter }
|
||||||
func (d *delayLinesParameter) Name(p *Parameter) string { return "delaylines" }
|
func (d *delayLinesParameter) Name(p *Parameter) string { return "delaylines" }
|
||||||
func (r *delayLinesParameter) RoundToGrid(p *Parameter, val int, up bool) int { return val }
|
func (r *delayLinesParameter) RoundToGrid(p *Parameter, val int, up bool) int { return val }
|
||||||
@@ -525,6 +610,20 @@ func (d *delayLinesParameter) Reset(p *Parameter) {}
|
|||||||
|
|
||||||
// reverbParameter vtable
|
// reverbParameter vtable
|
||||||
|
|
||||||
|
type delayPreset struct {
|
||||||
|
name string
|
||||||
|
stereo int
|
||||||
|
varArgs []int
|
||||||
|
}
|
||||||
|
|
||||||
|
var reverbs = []delayPreset{
|
||||||
|
{"stereo", 1, []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618,
|
||||||
|
1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642,
|
||||||
|
}},
|
||||||
|
{"left", 0, []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618}},
|
||||||
|
{"right", 0, []int{1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642}},
|
||||||
|
}
|
||||||
|
|
||||||
func (r *reverbParameter) Value(p *Parameter) int {
|
func (r *reverbParameter) Value(p *Parameter) int {
|
||||||
i := slices.IndexFunc(reverbs, func(d delayPreset) bool {
|
i := slices.IndexFunc(reverbs, func(d delayPreset) bool {
|
||||||
return d.stereo == p.unit.Parameters["stereo"] && p.unit.Parameters["notetracking"] == 0 && slices.Equal(d.varArgs, p.unit.VarArgs)
|
return d.stereo == p.unit.Parameters["stereo"] && p.unit.Parameters["notetracking"] == 0 && slices.Equal(d.varArgs, p.unit.VarArgs)
|
||||||
@@ -543,7 +642,9 @@ func (r *reverbParameter) SetValue(p *Parameter, v int) bool {
|
|||||||
copy(p.unit.VarArgs, entry.varArgs)
|
copy(p.unit.VarArgs, entry.varArgs)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
func (r *reverbParameter) Range(p *Parameter) IntRange { return IntRange{Min: 0, Max: len(reverbs)} }
|
func (r *reverbParameter) Range(p *Parameter) RangeInclusive {
|
||||||
|
return RangeInclusive{Min: 0, Max: len(reverbs)}
|
||||||
|
}
|
||||||
func (r *reverbParameter) Type(p *Parameter) ParameterType { return IntegerParameter }
|
func (r *reverbParameter) Type(p *Parameter) ParameterType { return IntegerParameter }
|
||||||
func (r *reverbParameter) Name(p *Parameter) string { return "reverb" }
|
func (r *reverbParameter) Name(p *Parameter) string { return "reverb" }
|
||||||
func (r *reverbParameter) RoundToGrid(p *Parameter, val int, up bool) int { return val }
|
func (r *reverbParameter) RoundToGrid(p *Parameter, val int, up bool) int { return val }
|
||||||
|
|||||||
193
tracker/play.go
Normal file
193
tracker/play.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import "github.com/vsariola/sointu"
|
||||||
|
|
||||||
|
type Play Model
|
||||||
|
|
||||||
|
func (m *Model) Play() *Play { return (*Play)(m) }
|
||||||
|
|
||||||
|
// Position returns the current play position as sointu.SongPos.
|
||||||
|
func (m *Play) Position() sointu.SongPos { return m.playerStatus.SongPos }
|
||||||
|
|
||||||
|
// Loop returns the current Loop telling which part of the song is looped.
|
||||||
|
func (m *Play) Loop() Loop { return m.loop }
|
||||||
|
|
||||||
|
// SongRow returns the current order row being played.
|
||||||
|
func (m *Play) SongRow() int { return m.d.Song.Score.SongRow(m.playerStatus.SongPos) }
|
||||||
|
|
||||||
|
// TrackerHidden returns a Bool controlling whether the tracker UI is hidden
|
||||||
|
// during playback (for example when recording).
|
||||||
|
func (m *Play) TrackerHidden() Bool { return MakeBoolFromPtr(&m.trackerHidden) }
|
||||||
|
|
||||||
|
// FromCurrentPos returns an Action to start playing the song from the current
|
||||||
|
// cursor position
|
||||||
|
func (m *Play) FromCurrentPos() Action { return MakeAction((*playCurrentPos)(m)) }
|
||||||
|
|
||||||
|
type playCurrentPos Play
|
||||||
|
|
||||||
|
func (m *playCurrentPos) Enabled() bool { return !m.trackerHidden }
|
||||||
|
func (m *playCurrentPos) Do() {
|
||||||
|
(*Model)(m).setPanic(false)
|
||||||
|
(*Model)(m).setLoop(Loop{})
|
||||||
|
m.playing = true
|
||||||
|
TrySend(m.broker.ToPlayer, any(StartPlayMsg{m.d.Cursor.SongPos}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromBeginning returns an Action to start playing the song from the beginning.
|
||||||
|
func (m *Play) FromBeginning() Action { return MakeAction((*playSongStart)(m)) }
|
||||||
|
|
||||||
|
type playSongStart Play
|
||||||
|
|
||||||
|
func (m *playSongStart) Enabled() bool { return !m.trackerHidden }
|
||||||
|
func (m *playSongStart) Do() {
|
||||||
|
(*Model)(m).setPanic(false)
|
||||||
|
(*Model)(m).setLoop(Loop{})
|
||||||
|
m.playing = true
|
||||||
|
TrySend(m.broker.ToPlayer, any(StartPlayMsg{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromSelected returns an Action to start playing and looping the currently
|
||||||
|
// selected patterns.
|
||||||
|
func (m *Play) FromSelected() Action { return MakeAction((*playSelected)(m)) }
|
||||||
|
|
||||||
|
type playSelected Play
|
||||||
|
|
||||||
|
func (m *playSelected) Enabled() bool { return !m.trackerHidden }
|
||||||
|
func (m *playSelected) Do() {
|
||||||
|
(*Model)(m).setPanic(false)
|
||||||
|
m.playing = true
|
||||||
|
l := (*Model)(m).Order().RowList()
|
||||||
|
r := l.listRange()
|
||||||
|
newLoop := Loop{r.Start, r.End - r.Start}
|
||||||
|
(*Model)(m).setLoop(newLoop)
|
||||||
|
TrySend(m.broker.ToPlayer, any(StartPlayMsg{sointu.SongPos{OrderRow: r.Start, PatternRow: 0}}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromLoopBeginning returns an Action to start playing from the beginning of the
|
||||||
|
func (m *Play) FromLoopBeginning() Action { return MakeAction((*playFromLoopStart)(m)) }
|
||||||
|
|
||||||
|
type playFromLoopStart Play
|
||||||
|
|
||||||
|
func (m *playFromLoopStart) Enabled() bool { return !m.trackerHidden }
|
||||||
|
func (m *playFromLoopStart) Do() {
|
||||||
|
(*Model)(m).setPanic(false)
|
||||||
|
if m.loop == (Loop{}) {
|
||||||
|
(*Play)(m).FromSelected().Do()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.playing = true
|
||||||
|
TrySend(m.broker.ToPlayer, any(StartPlayMsg{sointu.SongPos{OrderRow: m.loop.Start, PatternRow: 0}}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop returns an Action to stop playing the song.
|
||||||
|
func (m *Play) Stop() Action { return MakeAction((*stopPlaying)(m)) }
|
||||||
|
|
||||||
|
type stopPlaying Play
|
||||||
|
|
||||||
|
func (m *stopPlaying) Do() {
|
||||||
|
if !m.playing {
|
||||||
|
(*Model)(m).setPanic(true)
|
||||||
|
(*Model)(m).setLoop(Loop{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.playing = false
|
||||||
|
TrySend(m.broker.ToPlayer, any(IsPlayingMsg{false}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panicked returns a Bool to toggle whether the synth is in panic mode or not.
|
||||||
|
func (m *Play) Panicked() Bool { return MakeBool((*playPanicked)(m)) }
|
||||||
|
|
||||||
|
type playPanicked Model
|
||||||
|
|
||||||
|
func (m *playPanicked) Value() bool { return m.panic }
|
||||||
|
func (m *playPanicked) SetValue(val bool) { (*Model)(m).setPanic(val) }
|
||||||
|
|
||||||
|
// IsRecording returns a Bool to toggle whether recording is on or off.
|
||||||
|
func (m *Play) IsRecording() Bool { return MakeBool((*playIsRecording)(m)) }
|
||||||
|
|
||||||
|
type playIsRecording Model
|
||||||
|
|
||||||
|
func (m *playIsRecording) Value() bool { return (*Model)(m).recording }
|
||||||
|
func (m *playIsRecording) SetValue(val bool) {
|
||||||
|
m.recording = val
|
||||||
|
m.trackerHidden = val
|
||||||
|
TrySend(m.broker.ToPlayer, any(RecordingMsg{val}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Started returns a Bool to toggle whether playback has started or not.
|
||||||
|
func (m *Play) Started() Bool { return MakeBool((*playStarted)(m)) }
|
||||||
|
|
||||||
|
type playStarted Play
|
||||||
|
|
||||||
|
func (m *playStarted) Value() bool { return m.playing }
|
||||||
|
func (m *playStarted) SetValue(val bool) {
|
||||||
|
m.playing = val
|
||||||
|
if m.playing {
|
||||||
|
(*Model)(m).setPanic(false)
|
||||||
|
TrySend(m.broker.ToPlayer, any(StartPlayMsg{m.d.Cursor.SongPos}))
|
||||||
|
} else {
|
||||||
|
TrySend(m.broker.ToPlayer, any(IsPlayingMsg{val}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (m *playStarted) Enabled() bool { return m.playing || !m.trackerHidden }
|
||||||
|
|
||||||
|
// IsFollowing returns a Bool to toggle whether user cursors follows the
|
||||||
|
// playback cursor.
|
||||||
|
func (m *Play) IsFollowing() Bool { return MakeBoolFromPtr(&m.follow) }
|
||||||
|
|
||||||
|
// IsLooping returns a Bool to toggle whether looping is on or off.
|
||||||
|
func (m *Play) IsLooping() Bool { return MakeBool((*playIsLooping)(m)) }
|
||||||
|
|
||||||
|
type playIsLooping Play
|
||||||
|
|
||||||
|
func (m *playIsLooping) Value() bool { return m.loop.Length > 0 }
|
||||||
|
func (t *playIsLooping) SetValue(val bool) {
|
||||||
|
m := (*Model)(t)
|
||||||
|
newLoop := Loop{}
|
||||||
|
if val {
|
||||||
|
l := m.Order().RowList()
|
||||||
|
r := l.listRange()
|
||||||
|
newLoop = Loop{r.Start, r.End - r.Start}
|
||||||
|
}
|
||||||
|
m.setLoop(newLoop)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) setPanic(val bool) {
|
||||||
|
if m.panic != val {
|
||||||
|
m.panic = val
|
||||||
|
TrySend(m.broker.ToPlayer, any(PanicMsg{val}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) setLoop(newLoop Loop) {
|
||||||
|
if m.loop != newLoop {
|
||||||
|
m.loop = newLoop
|
||||||
|
TrySend(m.broker.ToPlayer, any(newLoop))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyntherIndex returns an Int representing the index of the currently selected
|
||||||
|
// synther.
|
||||||
|
func (m *Play) SyntherIndex() Int { return MakeInt((*playSyntherIndex)(m)) }
|
||||||
|
|
||||||
|
type playSyntherIndex Play
|
||||||
|
|
||||||
|
func (v *playSyntherIndex) Value() int { return v.syntherIndex }
|
||||||
|
func (v *playSyntherIndex) Range() RangeInclusive { return RangeInclusive{0, len(v.synthers) - 1} }
|
||||||
|
func (v *playSyntherIndex) SetValue(value int) bool {
|
||||||
|
if value < 0 || value >= len(v.synthers) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v.syntherIndex = value
|
||||||
|
TrySend(v.broker.ToPlayer, any(v.synthers[value]))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyntherName returns the name of the currently selected synther.
|
||||||
|
func (v *Play) SyntherName() string { return v.synthers[v.syntherIndex].Name() }
|
||||||
|
|
||||||
|
// CPULoad fills the given buffer with CPU load information and returns the
|
||||||
|
// number of threads filled.
|
||||||
|
func (m *Play) CPULoad(buf []sointu.CPULoad) int {
|
||||||
|
return copy(buf, m.playerStatus.CPULoad[:m.playerStatus.NumThreads])
|
||||||
|
}
|
||||||
@@ -19,75 +19,297 @@ import (
|
|||||||
//go:generate go run generate/gmdls_entries.go
|
//go:generate go run generate/gmdls_entries.go
|
||||||
//go:generate go run generate/clean_presets.go
|
//go:generate go run generate/clean_presets.go
|
||||||
|
|
||||||
//go:embed presets/*
|
// Preset returns a PresetModel, a view of the model used to manipulate
|
||||||
var instrumentPresetFS embed.FS
|
// instrument presets.
|
||||||
|
func (m *Model) Preset() *PresetModel { return (*PresetModel)(m) }
|
||||||
|
|
||||||
|
type PresetModel Model
|
||||||
|
|
||||||
|
// SearchTerm returns a String containing the search terms for finding the
|
||||||
|
// presets.
|
||||||
|
func (m *PresetModel) SearchTerm() String { return MakeString((*presetSearchTerm)(m)) }
|
||||||
|
|
||||||
|
type presetSearchTerm PresetModel
|
||||||
|
|
||||||
|
func (m *presetSearchTerm) Value() string { return m.d.PresetSearchString }
|
||||||
|
func (m *presetSearchTerm) SetValue(value string) bool {
|
||||||
|
if m.d.PresetSearchString == value {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m.d.PresetSearchString = value
|
||||||
|
(*PresetModel)(m).updateCache()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoGmDls returns a Bool toggling whether to show presets relying on gm.dls
|
||||||
|
// samples.
|
||||||
|
func (m *PresetModel) NoGmDls() Bool { return MakeBool((*presetNoGmDls)(m)) }
|
||||||
|
|
||||||
|
type presetNoGmDls PresetModel
|
||||||
|
|
||||||
|
func (m *presetNoGmDls) Value() bool { return m.presetData.cache.noGmDls }
|
||||||
|
func (m *presetNoGmDls) SetValue(val bool) {
|
||||||
|
if m.presetData.cache.noGmDls == val {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.d.PresetSearchString = removeFilters(m.d.PresetSearchString, "g:")
|
||||||
|
if val {
|
||||||
|
m.d.PresetSearchString = "g:n " + m.d.PresetSearchString
|
||||||
|
}
|
||||||
|
(*PresetModel)(m).updateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPresetsFilter returns a Bool toggling whether to show the user defined
|
||||||
|
// presets.
|
||||||
|
func (m *PresetModel) UserFilter() Bool { return MakeBool((*userPresetsFilter)(m)) }
|
||||||
|
|
||||||
|
type userPresetsFilter PresetModel
|
||||||
|
|
||||||
|
func (m *userPresetsFilter) Value() bool { return m.presetData.cache.kind == UserPresets }
|
||||||
|
func (m *userPresetsFilter) SetValue(val bool) {
|
||||||
|
if (m.presetData.cache.kind == UserPresets) == val {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.d.PresetSearchString = removeFilters(m.d.PresetSearchString, "t:")
|
||||||
|
if val {
|
||||||
|
m.d.PresetSearchString = "t:u " + m.d.PresetSearchString
|
||||||
|
}
|
||||||
|
(*PresetModel)(m).updateCache()
|
||||||
|
}
|
||||||
|
func (m *userPresetsFilter) Enabled() bool { return true }
|
||||||
|
|
||||||
|
// BuiltinFilter return a Bool toggling whether to show the built-in
|
||||||
|
// presets in the preset search results.
|
||||||
|
func (m *PresetModel) BuiltinFilter() Bool { return MakeBool((*builtinPresetsFilter)(m)) }
|
||||||
|
|
||||||
|
type builtinPresetsFilter PresetModel
|
||||||
|
|
||||||
|
func (m *builtinPresetsFilter) Value() bool { return m.presetData.cache.kind == BuiltinPresets }
|
||||||
|
func (m *builtinPresetsFilter) SetValue(val bool) {
|
||||||
|
if (m.presetData.cache.kind == BuiltinPresets) == val {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.d.PresetSearchString = removeFilters(m.d.PresetSearchString, "t:")
|
||||||
|
if val {
|
||||||
|
m.d.PresetSearchString = "t:b " + m.d.PresetSearchString
|
||||||
|
}
|
||||||
|
(*PresetModel)(m).updateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSearch returns an Action to clear the current preset search
|
||||||
|
// term(s).
|
||||||
|
func (m *PresetModel) ClearSearch() Action { return MakeAction((*clearPresetSearch)(m)) }
|
||||||
|
|
||||||
|
type clearPresetSearch PresetModel
|
||||||
|
|
||||||
|
func (m *clearPresetSearch) Enabled() bool { return len(m.d.PresetSearchString) > 0 }
|
||||||
|
func (m *clearPresetSearch) Do() {
|
||||||
|
m.d.PresetSearchString = ""
|
||||||
|
(*PresetModel)(m).updateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PresetDirList return a List of all the different preset directories.
|
||||||
|
func (m *PresetModel) DirList() List { return MakeList((*presetDirList)(m)) }
|
||||||
|
|
||||||
|
type presetDirList PresetModel
|
||||||
|
|
||||||
|
func (m *presetDirList) Count() int { return len(m.presetData.dirs) + 1 }
|
||||||
|
func (m *presetDirList) Selected() int { return m.presetData.cache.dirIndex + 1 }
|
||||||
|
func (m *presetDirList) Selected2() int { return m.presetData.cache.dirIndex + 1 }
|
||||||
|
func (m *presetDirList) SetSelected2(i int) {}
|
||||||
|
func (m *presetDirList) SetSelected(i int) {
|
||||||
|
i = min(max(i, 0), len(m.presetData.dirs))
|
||||||
|
if i < 0 || i > len(m.presetData.dirs) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.d.PresetSearchString = removeFilters(m.d.PresetSearchString, "d:")
|
||||||
|
if i > 0 {
|
||||||
|
m.d.PresetSearchString = "d:" + m.presetData.dirs[i-1] + " " + m.d.PresetSearchString
|
||||||
|
}
|
||||||
|
(*PresetModel)(m).updateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dir returns the name of the directory at the given index in the preset
|
||||||
|
// directory list.
|
||||||
|
func (m *PresetModel) Dir(i int) string {
|
||||||
|
if i < 1 || i > len(m.presetData.dirs) {
|
||||||
|
return "---"
|
||||||
|
}
|
||||||
|
return m.presetData.dirs[i-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchResultList returns a List of the current preset search results.
|
||||||
|
func (m *PresetModel) SearchResultList() List { return MakeList((*presetResultList)(m)) }
|
||||||
|
|
||||||
|
type presetResultList PresetModel
|
||||||
|
|
||||||
|
func (v *presetResultList) List() List { return List{v} }
|
||||||
|
func (m *presetResultList) Count() int { return len(m.presetData.cache.results) }
|
||||||
|
func (m *presetResultList) Selected() int {
|
||||||
|
return min(max(m.presetData.presetIndex, 0), len(m.presetData.cache.results)-1)
|
||||||
|
}
|
||||||
|
func (m *presetResultList) Selected2() int { return m.Selected() }
|
||||||
|
func (m *presetResultList) SetSelected2(i int) {}
|
||||||
|
func (m *presetResultList) SetSelected(i int) {
|
||||||
|
i = min(max(i, 0), len(m.presetData.cache.results)-1)
|
||||||
|
if i < 0 || i >= len(m.presetData.cache.results) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.presetData.presetIndex = i
|
||||||
|
defer (*Model)(m).change("LoadPreset", PatchChange, MinorChange)()
|
||||||
|
if m.d.InstrIndex < 0 {
|
||||||
|
m.d.InstrIndex = 0
|
||||||
|
}
|
||||||
|
m.d.InstrIndex2 = m.d.InstrIndex
|
||||||
|
for m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
|
||||||
|
}
|
||||||
|
newInstr := m.presetData.cache.results[i].instr.Copy()
|
||||||
|
newInstr.NumVoices = clamp(m.d.Song.Patch[m.d.InstrIndex].NumVoices, 1, vm.MAX_VOICES)
|
||||||
|
(*Model)(m).assignUnitIDs(newInstr.Units)
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex] = newInstr
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchResult returns the search result at the given index in the search
|
||||||
|
// result list.
|
||||||
|
func (m *PresetModel) SearchResult(i int) (name string, dir string, user bool) {
|
||||||
|
if i < 0 || i >= len(m.presetData.cache.results) {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
p := m.presetData.cache.results[i]
|
||||||
|
return p.instr.Name, p.dir, p.user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save returns an Action to save the current instrument as a user-defined
|
||||||
|
// preset. It will not overwrite existing presets, but rather show a dialog to
|
||||||
|
// confirm the overwrite.
|
||||||
|
func (m *PresetModel) Save() Action { return MakeAction((*saveUserPreset)(m)) }
|
||||||
|
|
||||||
|
type saveUserPreset PresetModel
|
||||||
|
|
||||||
|
func (m *saveUserPreset) Enabled() bool {
|
||||||
|
return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch)
|
||||||
|
}
|
||||||
|
func (m *saveUserPreset) Do() {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userPresetsDir := filepath.Join(configDir, "sointu", "presets", m.presetData.cache.dir)
|
||||||
|
instr := m.d.Song.Patch[m.d.InstrIndex]
|
||||||
|
name := instrumentNameToFilename(instr.Name)
|
||||||
|
fileName := filepath.Join(userPresetsDir, name+".yml")
|
||||||
|
// if exists, do not overwrite
|
||||||
|
if _, err := os.Stat(fileName); err == nil {
|
||||||
|
m.dialog = OverwriteUserPresetDialog
|
||||||
|
return
|
||||||
|
}
|
||||||
|
(*PresetModel)(m).Overwrite().Do()
|
||||||
|
}
|
||||||
|
|
||||||
|
// OverwriteUserPreset returns an Action to overwrite the current instrument
|
||||||
|
// as a user-defined preset.
|
||||||
|
func (m *PresetModel) Overwrite() Action { return MakeAction((*overwriteUserPreset)(m)) }
|
||||||
|
|
||||||
|
type overwriteUserPreset PresetModel
|
||||||
|
|
||||||
|
func (m *overwriteUserPreset) Enabled() bool { return true }
|
||||||
|
func (m *overwriteUserPreset) Do() {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userPresetsDir := filepath.Join(configDir, "sointu", "presets", m.presetData.cache.dir)
|
||||||
|
instr := m.d.Song.Patch[m.d.InstrIndex]
|
||||||
|
name := instrumentNameToFilename(instr.Name)
|
||||||
|
fileName := filepath.Join(userPresetsDir, name+".yml")
|
||||||
|
os.MkdirAll(userPresetsDir, 0755)
|
||||||
|
data, err := yaml.Marshal(&instr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.WriteFile(fileName, data, 0644)
|
||||||
|
m.dialog = NoDialog
|
||||||
|
(*PresetModel)(m).presetData.load()
|
||||||
|
(*PresetModel)(m).updateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryDeleteUserPreset returns an Action to display a dialog to confirm deletion
|
||||||
|
// of an user preset.
|
||||||
|
func (m *PresetModel) Delete() Action { return MakeAction((*tryDeleteUserPreset)(m)) }
|
||||||
|
|
||||||
|
type tryDeleteUserPreset PresetModel
|
||||||
|
|
||||||
|
func (m *tryDeleteUserPreset) Do() { m.dialog = DeleteUserPresetDialog }
|
||||||
|
func (m *tryDeleteUserPreset) Enabled() bool {
|
||||||
|
if m.presetData.presetIndex < 0 || m.presetData.presetIndex >= len(m.presetData.cache.results) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.presetData.cache.results[m.presetData.presetIndex].user
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUserPreset returns an Action to confirm the deletion of an user preset.
|
||||||
|
func (m *PresetModel) ConfirmDelete() Action { return MakeAction((*deleteUserPreset)(m)) }
|
||||||
|
|
||||||
|
type deleteUserPreset PresetModel
|
||||||
|
|
||||||
|
func (m *deleteUserPreset) Enabled() bool { return (*Model)(m).Preset().Delete().Enabled() }
|
||||||
|
func (m *deleteUserPreset) Do() {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p := m.presetData.cache.results[m.presetData.presetIndex]
|
||||||
|
userPresetsDir := filepath.Join(configDir, "sointu", "presets")
|
||||||
|
if p.dir != "" {
|
||||||
|
userPresetsDir = filepath.Join(userPresetsDir, p.dir)
|
||||||
|
}
|
||||||
|
name := instrumentNameToFilename(p.instr.Name)
|
||||||
|
fileName := filepath.Join(userPresetsDir, name+".yml")
|
||||||
|
os.Remove(fileName)
|
||||||
|
m.dialog = NoDialog
|
||||||
|
(*PresetModel)(m).presetData.load()
|
||||||
|
(*PresetModel)(m).updateCache()
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// GmDlsEntry is a single sample entry from the gm.dls file
|
presetData struct {
|
||||||
GmDlsEntry struct {
|
presets []preset
|
||||||
Start int // sample start offset in words
|
dirs []string
|
||||||
LoopStart int // loop start offset in words
|
presetIndex int
|
||||||
LoopLength int // loop length in words
|
|
||||||
SuggestedTranspose int // suggested transpose in semitones, so that all samples play at same pitch
|
cache presetCache
|
||||||
Name string // sample Name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Preset struct {
|
preset struct {
|
||||||
Directory string
|
dir string
|
||||||
User bool
|
user bool
|
||||||
NeedsGmDls bool
|
needsGmDls bool
|
||||||
Instr sointu.Instrument
|
instr sointu.Instrument
|
||||||
}
|
}
|
||||||
|
|
||||||
Presets struct {
|
presetCache struct {
|
||||||
Presets []Preset
|
|
||||||
Dirs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
InstrumentPresetYieldFunc func(index int, item string) (ok bool)
|
|
||||||
LoadPreset struct {
|
|
||||||
Index int
|
|
||||||
*Model
|
|
||||||
}
|
|
||||||
|
|
||||||
PresetSearchString Model
|
|
||||||
NoGmDlsFilter Model
|
|
||||||
BuiltinPresetsFilter Model
|
|
||||||
UserPresetsFilter Model
|
|
||||||
PresetDirectory Model
|
|
||||||
PresetKind Model
|
|
||||||
ClearPresetSearch Model
|
|
||||||
PresetDirList Model
|
|
||||||
PresetResultList Model
|
|
||||||
SaveUserPreset Model
|
|
||||||
TryDeleteUserPreset Model
|
|
||||||
DeleteUserPreset Model
|
|
||||||
|
|
||||||
ConfirmDeleteUserPresetAction Model
|
|
||||||
OverwriteUserPreset Model
|
|
||||||
|
|
||||||
derivedPresetSearch struct {
|
|
||||||
dir string
|
dir string
|
||||||
dirIndex int
|
dirIndex int
|
||||||
noGmDls bool
|
noGmDls bool
|
||||||
kind PresetKindEnum
|
kind presetKindEnum
|
||||||
searchStrings []string
|
searchStrings []string
|
||||||
results []Preset
|
results []preset
|
||||||
}
|
}
|
||||||
|
|
||||||
PresetKindEnum int
|
presetKindEnum int
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BuiltinPresets PresetKindEnum = -1
|
BuiltinPresets presetKindEnum = -1
|
||||||
AllPresets PresetKindEnum = 0
|
AllPresets presetKindEnum = 0
|
||||||
UserPresets PresetKindEnum = 1
|
UserPresets presetKindEnum = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Model) updateDerivedPresetSearch() {
|
func (m *PresetModel) updateCache() {
|
||||||
// reset derived data, keeping the
|
// reset derived data, keeping the
|
||||||
str := m.derived.presetSearch.searchStrings[:0]
|
str := m.presetData.cache.searchStrings[:0]
|
||||||
m.derived.presetSearch = derivedPresetSearch{searchStrings: str, dirIndex: -1}
|
m.presetData.cache = presetCache{searchStrings: str, dirIndex: -1}
|
||||||
// parse filters from the search string. in: dir, gmdls: yes/no, kind: builtin/user/all
|
// parse filters from the search string. in: dir, gmdls: yes/no, kind: builtin/user/all
|
||||||
search := strings.TrimSpace(m.d.PresetSearchString)
|
search := strings.TrimSpace(m.d.PresetSearchString)
|
||||||
parts := strings.Fields(search)
|
parts := strings.Fields(search)
|
||||||
@@ -95,69 +317,73 @@ func (m *Model) updateDerivedPresetSearch() {
|
|||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if strings.HasPrefix(part, "d:") && len(part) > 2 {
|
if strings.HasPrefix(part, "d:") && len(part) > 2 {
|
||||||
dir := strings.TrimSpace(part[2:])
|
dir := strings.TrimSpace(part[2:])
|
||||||
m.derived.presetSearch.dir = dir
|
m.presetData.cache.dir = dir
|
||||||
ind := slices.IndexFunc(m.presets.Dirs, func(c string) bool { return c == dir })
|
ind := slices.IndexFunc(m.presetData.dirs, func(c string) bool { return c == dir })
|
||||||
m.derived.presetSearch.dirIndex = ind
|
m.presetData.cache.dirIndex = ind
|
||||||
} else if strings.HasPrefix(part, "g:n") {
|
} else if strings.HasPrefix(part, "g:n") {
|
||||||
m.derived.presetSearch.noGmDls = true
|
m.presetData.cache.noGmDls = true
|
||||||
} else if strings.HasPrefix(part, "t:") && len(part) > 2 {
|
} else if strings.HasPrefix(part, "t:") && len(part) > 2 {
|
||||||
val := strings.TrimSpace(part[2:3])
|
val := strings.TrimSpace(part[2:3])
|
||||||
switch val {
|
switch val {
|
||||||
case "b":
|
case "b":
|
||||||
m.derived.presetSearch.kind = BuiltinPresets
|
m.presetData.cache.kind = BuiltinPresets
|
||||||
case "u":
|
case "u":
|
||||||
m.derived.presetSearch.kind = UserPresets
|
m.presetData.cache.kind = UserPresets
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m.derived.presetSearch.searchStrings = append(m.derived.presetSearch.searchStrings, strings.ToLower(part))
|
m.presetData.cache.searchStrings = append(m.presetData.cache.searchStrings, strings.ToLower(part))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// update results
|
// update results
|
||||||
m.derived.presetSearch.results = m.derived.presetSearch.results[:0]
|
m.presetData.cache.results = m.presetData.cache.results[:0]
|
||||||
for _, p := range m.presets.Presets {
|
for _, p := range m.presetData.presets {
|
||||||
if m.derived.presetSearch.kind == BuiltinPresets && p.User {
|
if m.presetData.cache.kind == BuiltinPresets && p.user {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if m.derived.presetSearch.kind == UserPresets && !p.User {
|
if m.presetData.cache.kind == UserPresets && !p.user {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if m.derived.presetSearch.dir != "" && p.Directory != m.derived.presetSearch.dir {
|
if m.presetData.cache.dir != "" && p.dir != m.presetData.cache.dir {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if m.derived.presetSearch.noGmDls && p.NeedsGmDls {
|
if m.presetData.cache.noGmDls && p.needsGmDls {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(m.derived.presetSearch.searchStrings) == 0 {
|
if len(m.presetData.cache.searchStrings) == 0 {
|
||||||
goto found
|
goto found
|
||||||
}
|
}
|
||||||
for _, s := range m.derived.presetSearch.searchStrings {
|
for _, s := range m.presetData.cache.searchStrings {
|
||||||
if strings.Contains(strings.ToLower(p.Instr.Name), s) {
|
if strings.Contains(strings.ToLower(p.instr.Name), s) {
|
||||||
goto found
|
goto found
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
found:
|
found:
|
||||||
m.derived.presetSearch.results = append(m.derived.presetSearch.results, p)
|
m.presetData.cache.results = append(m.presetData.cache.results, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Presets) load() {
|
//go:embed presets/*
|
||||||
*m = Presets{}
|
var builtInPresetsFS embed.FS
|
||||||
|
|
||||||
|
func (m *presetData) load() {
|
||||||
|
m.dirs = m.dirs[:0]
|
||||||
|
m.presets = m.presets[:0]
|
||||||
seenDir := make(map[string]bool)
|
seenDir := make(map[string]bool)
|
||||||
m.loadPresetsFromFs(instrumentPresetFS, false, seenDir)
|
m.loadPresetsFromFs(builtInPresetsFS, false, seenDir)
|
||||||
if configDir, err := os.UserConfigDir(); err == nil {
|
if configDir, err := os.UserConfigDir(); err == nil {
|
||||||
userPresets := filepath.Join(configDir, "sointu")
|
userPresets := filepath.Join(configDir, "sointu")
|
||||||
m.loadPresetsFromFs(os.DirFS(userPresets), true, seenDir)
|
m.loadPresetsFromFs(os.DirFS(userPresets), true, seenDir)
|
||||||
}
|
}
|
||||||
sort.Sort(m)
|
sort.Sort(m)
|
||||||
m.Dirs = make([]string, 0, len(seenDir))
|
m.dirs = make([]string, 0, len(seenDir))
|
||||||
for k := range seenDir {
|
for k := range seenDir {
|
||||||
m.Dirs = append(m.Dirs, k)
|
m.dirs = append(m.dirs, k)
|
||||||
}
|
}
|
||||||
sort.Strings(m.Dirs)
|
sort.Strings(m.dirs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Presets) loadPresetsFromFs(fsys fs.FS, userDefined bool, seenDir map[string]bool) {
|
func (m *presetData) loadPresetsFromFs(fsys fs.FS, userDefined bool, seenDir map[string]bool) {
|
||||||
fs.WalkDir(fsys, "presets", func(path string, d fs.DirEntry, err error) error {
|
fs.WalkDir(fsys, "presets", func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -179,16 +405,16 @@ func (m *Presets) loadPresetsFromFs(fsys fs.FS, userDefined bool, seenDir map[st
|
|||||||
splitted = splitted[1:] // remove "presets" from the path
|
splitted = splitted[1:] // remove "presets" from the path
|
||||||
instr.Name = filenameToInstrumentName(splitted[len(splitted)-1])
|
instr.Name = filenameToInstrumentName(splitted[len(splitted)-1])
|
||||||
dir := strings.Join(splitted[:len(splitted)-1], "/")
|
dir := strings.Join(splitted[:len(splitted)-1], "/")
|
||||||
preset := Preset{
|
preset := preset{
|
||||||
Directory: dir,
|
dir: dir,
|
||||||
User: userDefined,
|
user: userDefined,
|
||||||
Instr: instr,
|
instr: instr,
|
||||||
NeedsGmDls: checkNeedsGmDls(instr),
|
needsGmDls: checkNeedsGmDls(instr),
|
||||||
}
|
}
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
seenDir[dir] = true
|
seenDir[dir] = true
|
||||||
}
|
}
|
||||||
m.Presets = append(m.Presets, preset)
|
m.presets = append(m.presets, preset)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -217,125 +443,6 @@ func checkNeedsGmDls(instr sointu.Instrument) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) PresetSearchString() String { return MakeString((*PresetSearchString)(m)) }
|
|
||||||
func (m *PresetSearchString) Value() string { return m.d.PresetSearchString }
|
|
||||||
func (m *PresetSearchString) SetValue(value string) bool {
|
|
||||||
if m.d.PresetSearchString == value {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
m.d.PresetSearchString = value
|
|
||||||
(*Model)(m).updateDerivedPresetSearch()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) NoGmDls() Bool { return MakeBool((*NoGmDlsFilter)(m)) }
|
|
||||||
func (m *NoGmDlsFilter) Value() bool { return m.derived.presetSearch.noGmDls }
|
|
||||||
func (m *NoGmDlsFilter) SetValue(val bool) {
|
|
||||||
if m.derived.presetSearch.noGmDls == val {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.d.PresetSearchString = removeFilters(m.d.PresetSearchString, "g:")
|
|
||||||
if val {
|
|
||||||
m.d.PresetSearchString = "g:n " + m.d.PresetSearchString
|
|
||||||
}
|
|
||||||
(*Model)(m).updateDerivedPresetSearch()
|
|
||||||
}
|
|
||||||
func (m *NoGmDlsFilter) Enabled() bool { return true }
|
|
||||||
|
|
||||||
func (m *Model) UserPresetFilter() Bool { return MakeBool((*UserPresetsFilter)(m)) }
|
|
||||||
func (m *UserPresetsFilter) Value() bool { return m.derived.presetSearch.kind == UserPresets }
|
|
||||||
func (m *UserPresetsFilter) SetValue(val bool) {
|
|
||||||
if (m.derived.presetSearch.kind == UserPresets) == val {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.d.PresetSearchString = removeFilters(m.d.PresetSearchString, "t:")
|
|
||||||
if val {
|
|
||||||
m.d.PresetSearchString = "t:u " + m.d.PresetSearchString
|
|
||||||
}
|
|
||||||
(*Model)(m).updateDerivedPresetSearch()
|
|
||||||
}
|
|
||||||
func (m *UserPresetsFilter) Enabled() bool { return true }
|
|
||||||
|
|
||||||
func (m *Model) BuiltinPresetsFilter() Bool { return MakeBool((*BuiltinPresetsFilter)(m)) }
|
|
||||||
func (m *BuiltinPresetsFilter) Value() bool { return m.derived.presetSearch.kind == BuiltinPresets }
|
|
||||||
func (m *BuiltinPresetsFilter) SetValue(val bool) {
|
|
||||||
if (m.derived.presetSearch.kind == BuiltinPresets) == val {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.d.PresetSearchString = removeFilters(m.d.PresetSearchString, "t:")
|
|
||||||
if val {
|
|
||||||
m.d.PresetSearchString = "t:b " + m.d.PresetSearchString
|
|
||||||
}
|
|
||||||
(*Model)(m).updateDerivedPresetSearch()
|
|
||||||
}
|
|
||||||
func (m *BuiltinPresetsFilter) Enabled() bool { return true }
|
|
||||||
|
|
||||||
func (m *Model) ClearPresetSearch() Action { return MakeAction((*ClearPresetSearch)(m)) }
|
|
||||||
func (m *ClearPresetSearch) Enabled() bool { return len(m.d.PresetSearchString) > 0 }
|
|
||||||
func (m *ClearPresetSearch) Do() {
|
|
||||||
m.d.PresetSearchString = ""
|
|
||||||
(*Model)(m).updateDerivedPresetSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) PresetDirList() *PresetDirList { return (*PresetDirList)(m) }
|
|
||||||
func (v *PresetDirList) List() List { return List{v} }
|
|
||||||
func (m *PresetDirList) Count() int { return len(m.presets.Dirs) + 1 }
|
|
||||||
func (m *PresetDirList) Selected() int { return m.derived.presetSearch.dirIndex + 1 }
|
|
||||||
func (m *PresetDirList) Selected2() int { return m.derived.presetSearch.dirIndex + 1 }
|
|
||||||
func (m *PresetDirList) SetSelected2(i int) {}
|
|
||||||
func (m *PresetDirList) Value(i int) string {
|
|
||||||
if i < 1 || i > len(m.presets.Dirs) {
|
|
||||||
return "---"
|
|
||||||
}
|
|
||||||
return m.presets.Dirs[i-1]
|
|
||||||
}
|
|
||||||
func (m *PresetDirList) SetSelected(i int) {
|
|
||||||
i = min(max(i, 0), len(m.presets.Dirs))
|
|
||||||
if i < 0 || i > len(m.presets.Dirs) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.d.PresetSearchString = removeFilters(m.d.PresetSearchString, "d:")
|
|
||||||
if i > 0 {
|
|
||||||
m.d.PresetSearchString = "d:" + m.presets.Dirs[i-1] + " " + m.d.PresetSearchString
|
|
||||||
}
|
|
||||||
(*Model)(m).updateDerivedPresetSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) PresetResultList() *PresetResultList { return (*PresetResultList)(m) }
|
|
||||||
func (v *PresetResultList) List() List { return List{v} }
|
|
||||||
func (m *PresetResultList) Count() int { return len(m.derived.presetSearch.results) }
|
|
||||||
func (m *PresetResultList) Selected() int {
|
|
||||||
return min(max(m.presetIndex, 0), len(m.derived.presetSearch.results)-1)
|
|
||||||
}
|
|
||||||
func (m *PresetResultList) Selected2() int { return m.Selected() }
|
|
||||||
func (m *PresetResultList) SetSelected2(i int) {}
|
|
||||||
func (m *PresetResultList) Value(i int) (name string, dir string, user bool) {
|
|
||||||
if i < 0 || i >= len(m.derived.presetSearch.results) {
|
|
||||||
return "", "", false
|
|
||||||
}
|
|
||||||
p := m.derived.presetSearch.results[i]
|
|
||||||
return p.Instr.Name, p.Directory, p.User
|
|
||||||
}
|
|
||||||
func (m *PresetResultList) SetSelected(i int) {
|
|
||||||
i = min(max(i, 0), len(m.derived.presetSearch.results)-1)
|
|
||||||
if i < 0 || i >= len(m.derived.presetSearch.results) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.presetIndex = i
|
|
||||||
defer (*Model)(m).change("LoadPreset", PatchChange, MinorChange)()
|
|
||||||
if m.d.InstrIndex < 0 {
|
|
||||||
m.d.InstrIndex = 0
|
|
||||||
}
|
|
||||||
m.d.InstrIndex2 = m.d.InstrIndex
|
|
||||||
for m.d.InstrIndex >= len(m.d.Song.Patch) {
|
|
||||||
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
|
|
||||||
}
|
|
||||||
newInstr := m.derived.presetSearch.results[i].Instr.Copy()
|
|
||||||
newInstr.NumVoices = clamp(m.d.Song.Patch[m.d.InstrIndex].NumVoices, 1, vm.MAX_VOICES)
|
|
||||||
(*Model)(m).assignUnitIDs(newInstr.Units)
|
|
||||||
m.d.Song.Patch[m.d.InstrIndex] = newInstr
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeFilters(str string, prefix string) string {
|
func removeFilters(str string, prefix string) string {
|
||||||
parts := strings.Split(str, " ")
|
parts := strings.Split(str, " ")
|
||||||
newParts := make([]string, 0, len(parts))
|
newParts := make([]string, 0, len(parts))
|
||||||
@@ -347,175 +454,6 @@ func removeFilters(str string, prefix string) string {
|
|||||||
return strings.Join(newParts, " ")
|
return strings.Join(newParts, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) SaveAsUserPreset() Action { return MakeAction((*SaveUserPreset)(m)) }
|
|
||||||
func (m *SaveUserPreset) Enabled() bool {
|
|
||||||
return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch)
|
|
||||||
}
|
|
||||||
func (m *SaveUserPreset) Do() {
|
|
||||||
configDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userPresetsDir := filepath.Join(configDir, "sointu", "presets", m.derived.presetSearch.dir)
|
|
||||||
instr := m.d.Song.Patch[m.d.InstrIndex]
|
|
||||||
name := instrumentNameToFilename(instr.Name)
|
|
||||||
fileName := filepath.Join(userPresetsDir, name+".yml")
|
|
||||||
// if exists, do not overwrite
|
|
||||||
if _, err := os.Stat(fileName); err == nil {
|
|
||||||
m.dialog = OverwriteUserPresetDialog
|
|
||||||
return
|
|
||||||
}
|
|
||||||
(*Model)(m).OverwriteUserPreset().Do()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) OverwriteUserPreset() Action { return MakeAction((*OverwriteUserPreset)(m)) }
|
|
||||||
func (m *OverwriteUserPreset) Enabled() bool { return true }
|
|
||||||
func (m *OverwriteUserPreset) Do() {
|
|
||||||
configDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userPresetsDir := filepath.Join(configDir, "sointu", "presets", m.derived.presetSearch.dir)
|
|
||||||
instr := m.d.Song.Patch[m.d.InstrIndex]
|
|
||||||
name := instrumentNameToFilename(instr.Name)
|
|
||||||
fileName := filepath.Join(userPresetsDir, name+".yml")
|
|
||||||
os.MkdirAll(userPresetsDir, 0755)
|
|
||||||
data, err := yaml.Marshal(&instr)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
os.WriteFile(fileName, data, 0644)
|
|
||||||
m.dialog = NoDialog
|
|
||||||
(*Model)(m).presets.load()
|
|
||||||
(*Model)(m).updateDerivedPresetSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) TryDeleteUserPreset() Action { return MakeAction((*TryDeleteUserPreset)(m)) }
|
|
||||||
func (m *TryDeleteUserPreset) Do() { m.dialog = DeleteUserPresetDialog }
|
|
||||||
func (m *TryDeleteUserPreset) Enabled() bool {
|
|
||||||
if m.presetIndex < 0 || m.presetIndex >= len(m.derived.presetSearch.results) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return m.derived.presetSearch.results[m.presetIndex].User
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) DeleteUserPreset() Action { return MakeAction((*DeleteUserPreset)(m)) }
|
|
||||||
func (m *DeleteUserPreset) Enabled() bool { return (*Model)(m).TryDeleteUserPreset().Enabled() }
|
|
||||||
func (m *DeleteUserPreset) Do() {
|
|
||||||
configDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
p := m.derived.presetSearch.results[m.presetIndex]
|
|
||||||
userPresetsDir := filepath.Join(configDir, "sointu", "presets")
|
|
||||||
if p.Directory != "" {
|
|
||||||
userPresetsDir = filepath.Join(userPresetsDir, p.Directory)
|
|
||||||
}
|
|
||||||
name := instrumentNameToFilename(p.Instr.Name)
|
|
||||||
fileName := filepath.Join(userPresetsDir, name+".yml")
|
|
||||||
os.Remove(fileName)
|
|
||||||
m.dialog = NoDialog
|
|
||||||
(*Model)(m).presets.load()
|
|
||||||
(*Model)(m).updateDerivedPresetSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
// gmDlsEntryMap is a reverse map, to find the index of the GmDlsEntry in the
|
|
||||||
// GmDlsEntries list based on the sample offset. Do not modify during runtime.
|
|
||||||
var gmDlsEntryMap = make(map[vm.SampleOffset]int)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
for i, e := range GmDlsEntries {
|
|
||||||
key := vm.SampleOffset{Start: uint32(e.Start), LoopStart: uint16(e.LoopStart), LoopLength: uint16(e.LoopLength)}
|
|
||||||
gmDlsEntryMap[key] = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultUnits = map[string]sointu.Unit{
|
|
||||||
"envelope": {Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 64, "gain": 64}},
|
|
||||||
"oscillator": {Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 64, "shape": 64, "gain": 64, "type": sointu.Sine}},
|
|
||||||
"noise": {Type: "noise", Parameters: map[string]int{"stereo": 0, "shape": 64, "gain": 64}},
|
|
||||||
"mulp": {Type: "mulp", Parameters: map[string]int{"stereo": 0}},
|
|
||||||
"mul": {Type: "mul", Parameters: map[string]int{"stereo": 0}},
|
|
||||||
"add": {Type: "add", Parameters: map[string]int{"stereo": 0}},
|
|
||||||
"addp": {Type: "addp", Parameters: map[string]int{"stereo": 0}},
|
|
||||||
"push": {Type: "push", Parameters: map[string]int{"stereo": 0}},
|
|
||||||
"pop": {Type: "pop", Parameters: map[string]int{"stereo": 0}},
|
|
||||||
"xch": {Type: "xch", Parameters: map[string]int{"stereo": 0}},
|
|
||||||
"receive": {Type: "receive", Parameters: map[string]int{"stereo": 0}},
|
|
||||||
"loadnote": {Type: "loadnote", Parameters: map[string]int{"stereo": 0}},
|
|
||||||
"loadval": {Type: "loadval", Parameters: map[string]int{"stereo": 0, "value": 64}},
|
|
||||||
"pan": {Type: "pan", Parameters: map[string]int{"stereo": 0, "panning": 64}},
|
|
||||||
"gain": {Type: "gain", Parameters: map[string]int{"stereo": 0, "gain": 64}},
|
|
||||||
"invgain": {Type: "invgain", Parameters: map[string]int{"stereo": 0, "invgain": 64}},
|
|
||||||
"dbgain": {Type: "dbgain", Parameters: map[string]int{"stereo": 0, "decibels": 64}},
|
|
||||||
"crush": {Type: "crush", Parameters: map[string]int{"stereo": 0, "resolution": 64}},
|
|
||||||
"clip": {Type: "clip", Parameters: map[string]int{"stereo": 0}},
|
|
||||||
"hold": {Type: "hold", Parameters: map[string]int{"stereo": 0, "holdfreq": 64}},
|
|
||||||
"distort": {Type: "distort", Parameters: map[string]int{"stereo": 0, "drive": 64}},
|
|
||||||
"filter": {Type: "filter", Parameters: map[string]int{"stereo": 0, "frequency": 64, "resonance": 64, "lowpass": 1, "bandpass": 0, "highpass": 0}},
|
|
||||||
"out": {Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 64}},
|
|
||||||
"outaux": {Type: "outaux", Parameters: map[string]int{"stereo": 1, "outgain": 64, "auxgain": 64}},
|
|
||||||
"aux": {Type: "aux", Parameters: map[string]int{"stereo": 1, "gain": 64, "channel": 2}},
|
|
||||||
"delay": {Type: "delay",
|
|
||||||
Parameters: map[string]int{"damp": 0, "dry": 128, "feedback": 96, "notetracking": 2, "pregain": 40, "stereo": 0},
|
|
||||||
VarArgs: []int{48}},
|
|
||||||
"in": {Type: "in", Parameters: map[string]int{"stereo": 1, "channel": 2}},
|
|
||||||
"speed": {Type: "speed", Parameters: map[string]int{}},
|
|
||||||
"compressor": {Type: "compressor", Parameters: map[string]int{"stereo": 0, "attack": 64, "release": 64, "invgain": 64, "threshold": 64, "ratio": 64}},
|
|
||||||
"send": {Type: "send", Parameters: map[string]int{"stereo": 0, "amount": 64, "voice": 0, "unit": 0, "port": 0, "sendpop": 1}},
|
|
||||||
"sync": {Type: "sync", Parameters: map[string]int{}},
|
|
||||||
"belleq": {Type: "belleq", Parameters: map[string]int{"stereo": 0, "frequency": 64, "bandwidth": 64, "gain": 64}},
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultInstrument = sointu.Instrument{
|
|
||||||
Name: "Instr",
|
|
||||||
NumVoices: 1,
|
|
||||||
Units: []sointu.Unit{
|
|
||||||
defaultUnits["envelope"],
|
|
||||||
defaultUnits["oscillator"],
|
|
||||||
defaultUnits["mulp"],
|
|
||||||
defaultUnits["delay"],
|
|
||||||
defaultUnits["pan"],
|
|
||||||
defaultUnits["outaux"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultSong = sointu.Song{
|
|
||||||
BPM: 100,
|
|
||||||
RowsPerBeat: 4,
|
|
||||||
Score: sointu.Score{
|
|
||||||
RowsPerPattern: 16,
|
|
||||||
Length: 1,
|
|
||||||
Tracks: []sointu.Track{
|
|
||||||
{NumVoices: 1, Order: sointu.Order{0}, Patterns: []sointu.Pattern{{72, 0}}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Patch: sointu.Patch{defaultInstrument,
|
|
||||||
{Name: "Global", NumVoices: 1, Units: []sointu.Unit{
|
|
||||||
defaultUnits["in"],
|
|
||||||
{Type: "delay",
|
|
||||||
Parameters: map[string]int{"damp": 64, "dry": 128, "feedback": 125, "notetracking": 0, "pregain": 40, "stereo": 1},
|
|
||||||
VarArgs: []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618,
|
|
||||||
1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642,
|
|
||||||
}},
|
|
||||||
{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}},
|
|
||||||
}}},
|
|
||||||
}
|
|
||||||
|
|
||||||
var reverbs = []delayPreset{
|
|
||||||
{"stereo", 1, []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618,
|
|
||||||
1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642,
|
|
||||||
}},
|
|
||||||
{"left", 0, []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618}},
|
|
||||||
{"right", 0, []int{1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642}},
|
|
||||||
}
|
|
||||||
|
|
||||||
type delayPreset struct {
|
|
||||||
name string
|
|
||||||
stereo int
|
|
||||||
varArgs []int
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitPath(path string) []string {
|
func splitPath(path string) []string {
|
||||||
subPath := path
|
subPath := path
|
||||||
var result []string
|
var result []string
|
||||||
@@ -541,11 +479,11 @@ func splitPath(path string) []string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Presets) Len() int { return len(p.Presets) }
|
func (p presetData) Len() int { return len(p.presets) }
|
||||||
func (p Presets) Less(i, j int) bool {
|
func (p presetData) Less(i, j int) bool {
|
||||||
if p.Presets[i].Instr.Name == p.Presets[j].Instr.Name {
|
if p.presets[i].instr.Name == p.presets[j].instr.Name {
|
||||||
return p.Presets[i].User && !p.Presets[j].User
|
return p.presets[i].user && !p.presets[j].user
|
||||||
}
|
}
|
||||||
return p.Presets[i].Instr.Name < p.Presets[j].Instr.Name
|
return p.presets[i].instr.Name < p.presets[j].instr.Name
|
||||||
}
|
}
|
||||||
func (p Presets) Swap(i, j int) { p.Presets[i], p.Presets[j] = p.Presets[j], p.Presets[i] }
|
func (p presetData) Swap(i, j int) { p.presets[i], p.presets[j] = p.presets[j], p.presets[i] }
|
||||||
|
|||||||
127
tracker/scope.go
Normal file
127
tracker/scope.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
|
"github.com/vsariola/sointu/vm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scope returns the ScopeModel view of the Model, used for oscilloscope
|
||||||
|
// control.
|
||||||
|
func (m *Model) Scope() *ScopeModel { return (*ScopeModel)(m) }
|
||||||
|
|
||||||
|
type ScopeModel Model
|
||||||
|
|
||||||
|
type scopeData struct {
|
||||||
|
waveForm RingBuffer[[2]float32]
|
||||||
|
once bool
|
||||||
|
triggered bool
|
||||||
|
wrap bool
|
||||||
|
triggerChannel int
|
||||||
|
lengthInBeats int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once returns a Bool for controlling whether the oscilloscope should only
|
||||||
|
// trigger once.
|
||||||
|
func (m *ScopeModel) Once() Bool { return MakeBoolFromPtr(&m.scopeData.once) }
|
||||||
|
|
||||||
|
// Wrap returns a Bool for controlling whether the oscilloscope should wrap the
|
||||||
|
// buffer when full.
|
||||||
|
func (m *ScopeModel) Wrap() Bool { return MakeBoolFromPtr(&m.scopeData.wrap) }
|
||||||
|
|
||||||
|
// LengthInBeats returns an Int for controlling the length of the oscilloscope
|
||||||
|
// buffer in beats.
|
||||||
|
func (m *ScopeModel) LengthInBeats() Int { return MakeInt((*scopeLengthInBeats)(m)) }
|
||||||
|
|
||||||
|
type scopeLengthInBeats Model
|
||||||
|
|
||||||
|
func (s *scopeLengthInBeats) Value() int { return s.scopeData.lengthInBeats }
|
||||||
|
func (s *scopeLengthInBeats) SetValue(val int) bool {
|
||||||
|
s.scopeData.lengthInBeats = val
|
||||||
|
(*ScopeModel)(s).updateBufferLength()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (s *scopeLengthInBeats) Range() RangeInclusive { return RangeInclusive{1, 999} }
|
||||||
|
|
||||||
|
// TriggerChannel returns an Int for controlling the trigger channel of the
|
||||||
|
// oscilloscope. 0 = no trigger, 1 is the first channel etc.
|
||||||
|
func (m *ScopeModel) TriggerChannel() Int { return MakeInt((*scopeTriggerChannel)(m)) }
|
||||||
|
|
||||||
|
type scopeTriggerChannel Model
|
||||||
|
|
||||||
|
func (s *scopeTriggerChannel) Value() int { return s.scopeData.triggerChannel }
|
||||||
|
func (s *scopeTriggerChannel) SetValue(val int) bool {
|
||||||
|
s.scopeData.triggerChannel = val
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (s *scopeTriggerChannel) Range() RangeInclusive { return RangeInclusive{0, vm.MAX_VOICES} }
|
||||||
|
|
||||||
|
// Waveform returns the oscilloscope waveform buffer.
|
||||||
|
func (s *ScopeModel) Waveform() RingBuffer[[2]float32] { return s.scopeData.waveForm }
|
||||||
|
|
||||||
|
// processAudioBuffer fills the oscilloscope buffer with audio data from the
|
||||||
|
// given buffer.
|
||||||
|
func (s *ScopeModel) processAudioBuffer(bufPtr *sointu.AudioBuffer) {
|
||||||
|
if s.scopeData.wrap {
|
||||||
|
s.scopeData.waveForm.WriteWrap(*bufPtr)
|
||||||
|
} else {
|
||||||
|
s.scopeData.waveForm.WriteOnce(*bufPtr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger triggers the oscilloscope if the given channel matches the trigger
|
||||||
|
// channel.
|
||||||
|
func (s *ScopeModel) trigger(channel int) {
|
||||||
|
if s.scopeData.triggerChannel > 0 && channel == s.scopeData.triggerChannel && !(s.scopeData.once && s.scopeData.triggered) {
|
||||||
|
s.scopeData.waveForm.Cursor = 0
|
||||||
|
s.scopeData.triggered = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset resets the oscilloscope buffer and cursor.
|
||||||
|
func (s *ScopeModel) reset() {
|
||||||
|
s.scopeData.waveForm.Cursor = 0
|
||||||
|
s.scopeData.triggered = false
|
||||||
|
l := len(s.scopeData.waveForm.Buffer)
|
||||||
|
s.scopeData.waveForm.Buffer = s.scopeData.waveForm.Buffer[:0]
|
||||||
|
s.scopeData.waveForm.Buffer = append(s.scopeData.waveForm.Buffer, make([][2]float32, l)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScopeModel) updateBufferLength() {
|
||||||
|
if s.d.Song.BPM == 0 || s.scopeData.lengthInBeats == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSliceLength(&s.scopeData.waveForm.Buffer, s.d.Song.SamplesPerRow()*s.d.Song.RowsPerBeat*s.scopeData.lengthInBeats)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RingBuffer is a generic ring buffer with buffer and a cursor. It is used by
|
||||||
|
// the oscilloscope.
|
||||||
|
type RingBuffer[T any] struct {
|
||||||
|
Buffer []T
|
||||||
|
Cursor int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RingBuffer[T]) WriteWrap(values []T) {
|
||||||
|
r.Cursor = (r.Cursor + len(values)) % len(r.Buffer)
|
||||||
|
a := min(len(values), r.Cursor) // how many values to copy before the cursor
|
||||||
|
b := min(len(values)-a, len(r.Buffer)-r.Cursor) // how many values to copy to the end of the buffer
|
||||||
|
copy(r.Buffer[r.Cursor-a:r.Cursor], values[len(values)-a:])
|
||||||
|
copy(r.Buffer[len(r.Buffer)-b:], values[len(values)-a-b:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RingBuffer[T]) WriteWrapSingle(value T) {
|
||||||
|
r.Cursor = (r.Cursor + 1) % len(r.Buffer)
|
||||||
|
r.Buffer[r.Cursor] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RingBuffer[T]) WriteOnce(values []T) {
|
||||||
|
if r.Cursor < len(r.Buffer) {
|
||||||
|
r.Cursor += copy(r.Buffer[r.Cursor:], values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RingBuffer[T]) WriteOnceSingle(value T) {
|
||||||
|
if r.Cursor < len(r.Buffer) {
|
||||||
|
r.Buffer[r.Cursor] = value
|
||||||
|
r.Cursor++
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
package tracker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/vsariola/sointu"
|
|
||||||
"github.com/vsariola/sointu/vm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
ScopeModel struct {
|
|
||||||
waveForm RingBuffer[[2]float32]
|
|
||||||
once bool
|
|
||||||
triggered bool
|
|
||||||
wrap bool
|
|
||||||
triggerChannel int
|
|
||||||
lengthInBeats int
|
|
||||||
bpm int
|
|
||||||
}
|
|
||||||
|
|
||||||
RingBuffer[T any] struct {
|
|
||||||
Buffer []T
|
|
||||||
Cursor int
|
|
||||||
}
|
|
||||||
|
|
||||||
SignalOnce ScopeModel
|
|
||||||
SignalWrap ScopeModel
|
|
||||||
SignalLengthInBeats ScopeModel
|
|
||||||
TriggerChannel ScopeModel
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r *RingBuffer[T]) WriteWrap(values []T) {
|
|
||||||
r.Cursor = (r.Cursor + len(values)) % len(r.Buffer)
|
|
||||||
a := min(len(values), r.Cursor) // how many values to copy before the cursor
|
|
||||||
b := min(len(values)-a, len(r.Buffer)-r.Cursor) // how many values to copy to the end of the buffer
|
|
||||||
copy(r.Buffer[r.Cursor-a:r.Cursor], values[len(values)-a:])
|
|
||||||
copy(r.Buffer[len(r.Buffer)-b:], values[len(values)-a-b:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RingBuffer[T]) WriteWrapSingle(value T) {
|
|
||||||
r.Cursor = (r.Cursor + 1) % len(r.Buffer)
|
|
||||||
r.Buffer[r.Cursor] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RingBuffer[T]) WriteOnce(values []T) {
|
|
||||||
if r.Cursor < len(r.Buffer) {
|
|
||||||
r.Cursor += copy(r.Buffer[r.Cursor:], values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RingBuffer[T]) WriteOnceSingle(value T) {
|
|
||||||
if r.Cursor < len(r.Buffer) {
|
|
||||||
r.Buffer[r.Cursor] = value
|
|
||||||
r.Cursor++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewScopeModel(bpm int) *ScopeModel {
|
|
||||||
s := &ScopeModel{
|
|
||||||
bpm: bpm,
|
|
||||||
lengthInBeats: 4,
|
|
||||||
}
|
|
||||||
s.updateBufferLength()
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ScopeModel) Waveform() RingBuffer[[2]float32] { return s.waveForm }
|
|
||||||
|
|
||||||
func (s *ScopeModel) Once() Bool { return MakeBool((*SignalOnce)(s)) }
|
|
||||||
func (s *ScopeModel) Wrap() Bool { return MakeBool((*SignalWrap)(s)) }
|
|
||||||
func (s *ScopeModel) LengthInBeats() Int { return MakeInt((*SignalLengthInBeats)(s)) }
|
|
||||||
func (s *ScopeModel) TriggerChannel() Int { return MakeInt((*TriggerChannel)(s)) }
|
|
||||||
|
|
||||||
func (m *SignalOnce) Value() bool { return m.once }
|
|
||||||
func (m *SignalOnce) SetValue(val bool) { m.once = val }
|
|
||||||
|
|
||||||
func (m *SignalWrap) Value() bool { return m.wrap }
|
|
||||||
func (m *SignalWrap) SetValue(val bool) { m.wrap = val }
|
|
||||||
|
|
||||||
func (m *SignalLengthInBeats) Value() int { return m.lengthInBeats }
|
|
||||||
func (m *SignalLengthInBeats) SetValue(val int) bool {
|
|
||||||
m.lengthInBeats = val
|
|
||||||
(*ScopeModel)(m).updateBufferLength()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
func (m *SignalLengthInBeats) Range() IntRange { return IntRange{1, 999} }
|
|
||||||
|
|
||||||
func (m *TriggerChannel) Value() int { return m.triggerChannel }
|
|
||||||
func (m *TriggerChannel) SetValue(val int) bool { m.triggerChannel = val; return true }
|
|
||||||
func (m *TriggerChannel) Range() IntRange { return IntRange{0, vm.MAX_VOICES} }
|
|
||||||
|
|
||||||
func (s *ScopeModel) ProcessAudioBuffer(bufPtr *sointu.AudioBuffer) {
|
|
||||||
if s.wrap {
|
|
||||||
s.waveForm.WriteWrap(*bufPtr)
|
|
||||||
} else {
|
|
||||||
s.waveForm.WriteOnce(*bufPtr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: channel 1 is the first channel
|
|
||||||
func (s *ScopeModel) Trigger(channel int) {
|
|
||||||
if s.triggerChannel > 0 && channel == s.triggerChannel && !(s.once && s.triggered) {
|
|
||||||
s.waveForm.Cursor = 0
|
|
||||||
s.triggered = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ScopeModel) Reset() {
|
|
||||||
s.waveForm.Cursor = 0
|
|
||||||
s.triggered = false
|
|
||||||
l := len(s.waveForm.Buffer)
|
|
||||||
s.waveForm.Buffer = s.waveForm.Buffer[:0]
|
|
||||||
s.waveForm.Buffer = append(s.waveForm.Buffer, make([][2]float32, l)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ScopeModel) SetBpm(bpm int) {
|
|
||||||
s.bpm = bpm
|
|
||||||
s.updateBufferLength()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ScopeModel) updateBufferLength() {
|
|
||||||
if s.bpm == 0 || s.lengthInBeats == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setSliceLength(&s.waveForm.Buffer, 44100*60*s.lengthInBeats/s.bpm)
|
|
||||||
}
|
|
||||||
367
tracker/song.go
Normal file
367
tracker/song.go
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Song returns the Song view of the model, containing methods to manipulate the
|
||||||
|
// song.
|
||||||
|
func (m *Model) Song() *SongModel { return (*SongModel)(m) }
|
||||||
|
|
||||||
|
type SongModel Model
|
||||||
|
|
||||||
|
// FilePath returns a String representing the file path of the current song.
|
||||||
|
func (m *SongModel) FilePath() String { return MakeString((*songFilePath)(m)) }
|
||||||
|
|
||||||
|
type songFilePath SongModel
|
||||||
|
|
||||||
|
func (v *songFilePath) Value() string { return v.d.FilePath }
|
||||||
|
func (v *songFilePath) SetValue(value string) bool { v.d.FilePath = value; return true }
|
||||||
|
|
||||||
|
// BPM returns an Int representing the BPM of the current song.
|
||||||
|
func (m *SongModel) BPM() Int { return MakeInt((*songBpm)(m)) }
|
||||||
|
|
||||||
|
type songBpm SongModel
|
||||||
|
|
||||||
|
func (v *songBpm) Value() int { return v.d.Song.BPM }
|
||||||
|
func (v *songBpm) SetValue(value int) bool {
|
||||||
|
defer (*Model)(v).change("BPMInt", SongChange, MinorChange)()
|
||||||
|
v.d.Song.BPM = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (v *songBpm) Range() RangeInclusive { return RangeInclusive{1, 999} }
|
||||||
|
|
||||||
|
// RowsPerPattern returns an Int representing the number of rows per pattern of
|
||||||
|
// the current song.
|
||||||
|
func (m *SongModel) RowsPerPattern() Int { return MakeInt((*songRowsPerPattern)(m)) }
|
||||||
|
|
||||||
|
type songRowsPerPattern SongModel
|
||||||
|
|
||||||
|
func (v *songRowsPerPattern) Value() int { return v.d.Song.Score.RowsPerPattern }
|
||||||
|
func (v *songRowsPerPattern) SetValue(value int) bool {
|
||||||
|
defer (*Model)(v).change("RowsPerPatternInt", SongChange, MinorChange)()
|
||||||
|
v.d.Song.Score.RowsPerPattern = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (v *songRowsPerPattern) Range() RangeInclusive { return RangeInclusive{1, 256} }
|
||||||
|
|
||||||
|
// Length returns an Int representing the length of the current song, in number
|
||||||
|
// of order rows.
|
||||||
|
func (m *SongModel) Length() Int { return MakeInt((*songLength)(m)) }
|
||||||
|
|
||||||
|
type songLength SongModel
|
||||||
|
|
||||||
|
func (v *songLength) Value() int { return v.d.Song.Score.Length }
|
||||||
|
func (v *songLength) SetValue(value int) bool {
|
||||||
|
defer (*Model)(v).change("SongLengthInt", SongChange, MinorChange)()
|
||||||
|
v.d.Song.Score.Length = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (v *songLength) Range() RangeInclusive { return RangeInclusive{1, math.MaxInt32} }
|
||||||
|
|
||||||
|
// RowsPerBeat returns an Int representing the number of rows per beat of the
|
||||||
|
// current song.
|
||||||
|
func (m *SongModel) RowsPerBeat() Int { return MakeInt((*songRowsPerBeat)(m)) }
|
||||||
|
|
||||||
|
type songRowsPerBeat SongModel
|
||||||
|
|
||||||
|
func (v *songRowsPerBeat) Value() int { return v.d.Song.RowsPerBeat }
|
||||||
|
func (v *songRowsPerBeat) SetValue(value int) bool {
|
||||||
|
defer (*Model)(v).change("RowsPerBeatInt", SongChange, MinorChange)()
|
||||||
|
v.d.Song.RowsPerBeat = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (v *songRowsPerBeat) Range() RangeInclusive { return RangeInclusive{1, 32} }
|
||||||
|
|
||||||
|
// Save returns an Action to initiate saving the current song to disk.
|
||||||
|
func (m *SongModel) Save() Action { return MakeAction((*saveSong)(m)) }
|
||||||
|
|
||||||
|
type saveSong Model
|
||||||
|
|
||||||
|
func (m *saveSong) Do() {
|
||||||
|
if m.d.FilePath == "" {
|
||||||
|
switch m.dialog {
|
||||||
|
case NoDialog:
|
||||||
|
m.dialog = SaveAsExplorer
|
||||||
|
case NewSongChanges:
|
||||||
|
m.dialog = NewSongSaveExplorer
|
||||||
|
case OpenSongChanges:
|
||||||
|
m.dialog = OpenSongSaveExplorer
|
||||||
|
case QuitChanges:
|
||||||
|
m.dialog = QuitSaveExplorer
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f, err := os.Create(m.d.FilePath)
|
||||||
|
if err != nil {
|
||||||
|
(*Model)(m).Alerts().Add("Error creating file: "+err.Error(), Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
(*Model)(m).Song().Write(f)
|
||||||
|
m.d.ChangedSinceSave = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns an Action to create a new song.
|
||||||
|
func (m *SongModel) New() Action { return MakeAction((*newSong)(m)) }
|
||||||
|
|
||||||
|
type newSong SongModel
|
||||||
|
|
||||||
|
func (m *newSong) Do() {
|
||||||
|
m.dialog = NewSongChanges
|
||||||
|
(*SongModel)(m).completeAction(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SongModel) completeAction(checkSave bool) {
|
||||||
|
if checkSave && m.d.ChangedSinceSave {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch m.dialog {
|
||||||
|
case NewSongChanges, NewSongSaveExplorer:
|
||||||
|
c := (*Model)(m).change("NewSong", SongChange, MajorChange)
|
||||||
|
m.reset()
|
||||||
|
(*Model)(m).setLoop(Loop{})
|
||||||
|
c()
|
||||||
|
m.d.ChangedSinceSave = false
|
||||||
|
m.dialog = NoDialog
|
||||||
|
case OpenSongChanges, OpenSongSaveExplorer:
|
||||||
|
m.dialog = OpenSongOpenExplorer
|
||||||
|
case QuitChanges, QuitSaveExplorer:
|
||||||
|
m.quitted = true
|
||||||
|
m.dialog = NoDialog
|
||||||
|
default:
|
||||||
|
m.dialog = NoDialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SongModel) reset() {
|
||||||
|
m.d.Song = defaultSong.Copy()
|
||||||
|
for _, instr := range m.d.Song.Patch {
|
||||||
|
(*Model)(m).assignUnitIDs(instr.Units)
|
||||||
|
}
|
||||||
|
m.d.FilePath = ""
|
||||||
|
m.d.ChangedSinceSave = false
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultUnits = map[string]sointu.Unit{
|
||||||
|
"envelope": {Type: "envelope", Parameters: map[string]int{"stereo": 0, "attack": 64, "decay": 64, "sustain": 64, "release": 64, "gain": 64}},
|
||||||
|
"oscillator": {Type: "oscillator", Parameters: map[string]int{"stereo": 0, "transpose": 64, "detune": 64, "phase": 0, "color": 64, "shape": 64, "gain": 64, "type": sointu.Sine}},
|
||||||
|
"noise": {Type: "noise", Parameters: map[string]int{"stereo": 0, "shape": 64, "gain": 64}},
|
||||||
|
"mulp": {Type: "mulp", Parameters: map[string]int{"stereo": 0}},
|
||||||
|
"mul": {Type: "mul", Parameters: map[string]int{"stereo": 0}},
|
||||||
|
"add": {Type: "add", Parameters: map[string]int{"stereo": 0}},
|
||||||
|
"addp": {Type: "addp", Parameters: map[string]int{"stereo": 0}},
|
||||||
|
"push": {Type: "push", Parameters: map[string]int{"stereo": 0}},
|
||||||
|
"pop": {Type: "pop", Parameters: map[string]int{"stereo": 0}},
|
||||||
|
"xch": {Type: "xch", Parameters: map[string]int{"stereo": 0}},
|
||||||
|
"receive": {Type: "receive", Parameters: map[string]int{"stereo": 0}},
|
||||||
|
"loadnote": {Type: "loadnote", Parameters: map[string]int{"stereo": 0}},
|
||||||
|
"loadval": {Type: "loadval", Parameters: map[string]int{"stereo": 0, "value": 64}},
|
||||||
|
"pan": {Type: "pan", Parameters: map[string]int{"stereo": 0, "panning": 64}},
|
||||||
|
"gain": {Type: "gain", Parameters: map[string]int{"stereo": 0, "gain": 64}},
|
||||||
|
"invgain": {Type: "invgain", Parameters: map[string]int{"stereo": 0, "invgain": 64}},
|
||||||
|
"dbgain": {Type: "dbgain", Parameters: map[string]int{"stereo": 0, "decibels": 64}},
|
||||||
|
"crush": {Type: "crush", Parameters: map[string]int{"stereo": 0, "resolution": 64}},
|
||||||
|
"clip": {Type: "clip", Parameters: map[string]int{"stereo": 0}},
|
||||||
|
"hold": {Type: "hold", Parameters: map[string]int{"stereo": 0, "holdfreq": 64}},
|
||||||
|
"distort": {Type: "distort", Parameters: map[string]int{"stereo": 0, "drive": 64}},
|
||||||
|
"filter": {Type: "filter", Parameters: map[string]int{"stereo": 0, "frequency": 64, "resonance": 64, "lowpass": 1, "bandpass": 0, "highpass": 0}},
|
||||||
|
"out": {Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 64}},
|
||||||
|
"outaux": {Type: "outaux", Parameters: map[string]int{"stereo": 1, "outgain": 64, "auxgain": 64}},
|
||||||
|
"aux": {Type: "aux", Parameters: map[string]int{"stereo": 1, "gain": 64, "channel": 2}},
|
||||||
|
"delay": {Type: "delay",
|
||||||
|
Parameters: map[string]int{"damp": 0, "dry": 128, "feedback": 96, "notetracking": 2, "pregain": 40, "stereo": 0},
|
||||||
|
VarArgs: []int{48}},
|
||||||
|
"in": {Type: "in", Parameters: map[string]int{"stereo": 1, "channel": 2}},
|
||||||
|
"speed": {Type: "speed", Parameters: map[string]int{}},
|
||||||
|
"compressor": {Type: "compressor", Parameters: map[string]int{"stereo": 0, "attack": 64, "release": 64, "invgain": 64, "threshold": 64, "ratio": 64}},
|
||||||
|
"send": {Type: "send", Parameters: map[string]int{"stereo": 0, "amount": 64, "voice": 0, "unit": 0, "port": 0, "sendpop": 1}},
|
||||||
|
"sync": {Type: "sync", Parameters: map[string]int{}},
|
||||||
|
"belleq": {Type: "belleq", Parameters: map[string]int{"stereo": 0, "frequency": 64, "bandwidth": 64, "gain": 64}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultInstrument = sointu.Instrument{
|
||||||
|
Name: "Instr",
|
||||||
|
NumVoices: 1,
|
||||||
|
Units: []sointu.Unit{
|
||||||
|
defaultUnits["envelope"],
|
||||||
|
defaultUnits["oscillator"],
|
||||||
|
defaultUnits["mulp"],
|
||||||
|
defaultUnits["delay"],
|
||||||
|
defaultUnits["pan"],
|
||||||
|
defaultUnits["outaux"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultSong = sointu.Song{
|
||||||
|
BPM: 100,
|
||||||
|
RowsPerBeat: 4,
|
||||||
|
Score: sointu.Score{
|
||||||
|
RowsPerPattern: 16,
|
||||||
|
Length: 1,
|
||||||
|
Tracks: []sointu.Track{
|
||||||
|
{NumVoices: 1, Order: sointu.Order{0}, Patterns: []sointu.Pattern{{72, 0}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Patch: sointu.Patch{defaultInstrument,
|
||||||
|
{Name: "Global", NumVoices: 1, Units: []sointu.Unit{
|
||||||
|
defaultUnits["in"],
|
||||||
|
{Type: "delay",
|
||||||
|
Parameters: map[string]int{"damp": 64, "dry": 128, "feedback": 125, "notetracking": 0, "pregain": 40, "stereo": 1},
|
||||||
|
VarArgs: []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618,
|
||||||
|
1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642,
|
||||||
|
}},
|
||||||
|
{Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 128}},
|
||||||
|
}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open returns an Action to open a song from the disk.
|
||||||
|
func (m *SongModel) Open() Action { return MakeAction((*openSong)(m)) }
|
||||||
|
|
||||||
|
type openSong SongModel
|
||||||
|
|
||||||
|
func (m *openSong) Do() {
|
||||||
|
m.dialog = OpenSongChanges
|
||||||
|
(*SongModel)(m).completeAction(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAs returns an Action to save the song to the disk with a new filename.
|
||||||
|
func (m *SongModel) SaveAs() Action { return MakeAction((*saveSongAs)(m)) }
|
||||||
|
|
||||||
|
type saveSongAs SongModel
|
||||||
|
|
||||||
|
func (m *saveSongAs) Do() { m.dialog = SaveAsExplorer }
|
||||||
|
|
||||||
|
// Discard returns an Action to discard the current changes to the song when
|
||||||
|
// opening a song from disk or creating a new one.
|
||||||
|
func (m *SongModel) Discard() Action { return MakeAction((*discardSong)(m)) }
|
||||||
|
|
||||||
|
type discardSong SongModel
|
||||||
|
|
||||||
|
func (m *discardSong) Do() { (*SongModel)(m).completeAction(false) }
|
||||||
|
|
||||||
|
// Read the song from a given io.ReadCloser, trying parsing it both as json and
|
||||||
|
// yaml.
|
||||||
|
func (m *SongModel) Read(r io.ReadCloser) {
|
||||||
|
b, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var song sointu.Song
|
||||||
|
if errJSON := json.Unmarshal(b, &song); errJSON != nil {
|
||||||
|
if errYaml := yaml.Unmarshal(b, &song); errYaml != nil {
|
||||||
|
(*Model)(m).Alerts().Add(fmt.Sprintf("Error unmarshaling a song file: %v / %v", errYaml, errJSON), Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f := (*Model)(m).change("LoadSong", SongChange, MajorChange)
|
||||||
|
m.d.Song = song
|
||||||
|
if f, ok := r.(*os.File); ok {
|
||||||
|
m.d.FilePath = f.Name()
|
||||||
|
// when the song is loaded from a file, we are quite confident that the file is persisted and thus
|
||||||
|
// we can close sointu without worrying about losing changes
|
||||||
|
m.d.ChangedSinceSave = false
|
||||||
|
}
|
||||||
|
f()
|
||||||
|
(*SongModel)(m).completeAction(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the song to a given io.ReadCloser. If the given argument is an os.File
|
||||||
|
// and has the file extension ".json", the song is marshaled as json; otherwise,
|
||||||
|
// it's marshaled as yaml.
|
||||||
|
func (m *SongModel) Write(w io.WriteCloser) {
|
||||||
|
path := ""
|
||||||
|
var extension = filepath.Ext(path)
|
||||||
|
var contents []byte
|
||||||
|
var err error
|
||||||
|
if extension == ".json" {
|
||||||
|
contents, err = json.Marshal(m.d.Song)
|
||||||
|
} else {
|
||||||
|
contents, err = yaml.Marshal(m.d.Song)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
(*Model)(m).Alerts().Add(fmt.Sprintf("Error marshaling a song file: %v", err), Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := w.Write(contents); err != nil {
|
||||||
|
(*Model)(m).Alerts().Add(fmt.Sprintf("Error writing to file: %v", err), Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if f, ok := w.(*os.File); ok {
|
||||||
|
path = f.Name()
|
||||||
|
// when the song is saved to a file, we are quite confident that the file is persisted and thus
|
||||||
|
// we can close sointu without worrying about losing changes
|
||||||
|
m.d.ChangedSinceSave = false
|
||||||
|
}
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
(*Model)(m).Alerts().Add(fmt.Sprintf("Error closing the song file: %v", err), Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.d.FilePath = path
|
||||||
|
(*SongModel)(m).completeAction(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export returns an Action to show the wav export dialog.
|
||||||
|
func (m *SongModel) Export() Action { return MakeAction((*exportAction)(m)) }
|
||||||
|
|
||||||
|
type exportAction SongModel
|
||||||
|
|
||||||
|
func (m *exportAction) Do() { m.dialog = Export }
|
||||||
|
|
||||||
|
// ExportFloat returns an Action to start exporting the song as a wav file with
|
||||||
|
// 32-bit float samples.
|
||||||
|
func (m *SongModel) ExportFloat() Action { return MakeAction((*exportFloat)(m)) }
|
||||||
|
|
||||||
|
type exportFloat SongModel
|
||||||
|
|
||||||
|
func (m *exportFloat) Do() { m.dialog = ExportFloatExplorer }
|
||||||
|
|
||||||
|
// ExportInt16 returns an Action to start exporting the song as a wav file with
|
||||||
|
// 16-bit integer samples.
|
||||||
|
func (m *SongModel) ExportInt16() Action { return MakeAction((*exportInt16)(m)) }
|
||||||
|
|
||||||
|
type exportInt16 SongModel
|
||||||
|
|
||||||
|
func (m *exportInt16) Do() { m.dialog = ExportInt16Explorer }
|
||||||
|
|
||||||
|
// WriteWav renders the song as a wav file and outputs it to the given
|
||||||
|
// io.WriteCloser. If the pcm16 is true, the sample format is 16-bit unsigned
|
||||||
|
// shorts, otherwise it's 32-bit floats.
|
||||||
|
func (m *SongModel) WriteWav(w io.WriteCloser, pcm16 bool) {
|
||||||
|
m.dialog = NoDialog
|
||||||
|
song := m.d.Song.Copy()
|
||||||
|
go func() {
|
||||||
|
b := make([]byte, 32+2)
|
||||||
|
rand.Read(b)
|
||||||
|
name := fmt.Sprintf("%x", b)[2 : 32+2]
|
||||||
|
data, err := sointu.Play(m.synthers[m.syntherIndex], song, func(p float32) {
|
||||||
|
txt := fmt.Sprintf("Exporting song: %.0f%%", p*100)
|
||||||
|
TrySend(m.broker.ToModel, MsgToModel{Data: Alert{Message: txt, Priority: Info, Name: name, Duration: defaultAlertDuration}})
|
||||||
|
}) // render the song to calculate its length
|
||||||
|
if err != nil {
|
||||||
|
txt := fmt.Sprintf("Error rendering the song during export: %v", err)
|
||||||
|
TrySend(m.broker.ToModel, MsgToModel{Data: Alert{Message: txt, Priority: Error, Name: name, Duration: defaultAlertDuration}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buffer, err := data.Wav(pcm16)
|
||||||
|
if err != nil {
|
||||||
|
txt := fmt.Sprintf("Error converting to .wav: %v", err)
|
||||||
|
TrySend(m.broker.ToModel, MsgToModel{Data: Alert{Message: txt, Priority: Error, Name: name, Duration: defaultAlertDuration}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(buffer)
|
||||||
|
w.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -8,49 +8,71 @@ import (
|
|||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
// Spectrum returns a SpectrumModel to access spectrum analyzer data and
|
||||||
SpecAnalyzer struct {
|
// settings.
|
||||||
settings SpecAnSettings
|
func (m *Model) Spectrum() *SpectrumModel { return (*SpectrumModel)(m) }
|
||||||
broker *Broker
|
|
||||||
chunker chunker
|
|
||||||
temp specTemp
|
|
||||||
}
|
|
||||||
|
|
||||||
SpecAnSettings struct {
|
type SpectrumModel Model
|
||||||
ChnMode SpecChnMode
|
|
||||||
Smooth int
|
|
||||||
Resolution int
|
|
||||||
}
|
|
||||||
|
|
||||||
SpecChnMode int
|
// Result returns the latest spectrum analyzer result.
|
||||||
Spectrum [2][]float32
|
func (m *SpectrumModel) Result() Spectrum { return *m.spectrum }
|
||||||
|
|
||||||
specTemp struct {
|
type Spectrum [2][]float32
|
||||||
power [2][]float32
|
|
||||||
window []float32 // window weighting function
|
|
||||||
normFactor float32 // normalization factor, to account for the windowing
|
|
||||||
bitPerm []int // bit-reversal permutation table
|
|
||||||
tmpC []complex128 // temporary buffer for FFT
|
|
||||||
tmp1, tmp2 []float32 // temporary buffers for processing
|
|
||||||
}
|
|
||||||
|
|
||||||
BiquadCoeffs struct {
|
// Speed returns an Int to adjust the smoothing speed of the spectrum analyzer.
|
||||||
b0, b1, b2 float32
|
func (m *SpectrumModel) Speed() Int { return MakeInt((*spectrumSpeed)(m)) }
|
||||||
a0, a1, a2 float32
|
|
||||||
}
|
|
||||||
|
|
||||||
SpecAnEnabled Model
|
type spectrumSpeed Model
|
||||||
|
|
||||||
|
func (v *spectrumSpeed) Value() int { return int(v.specAnSettings.Smooth) }
|
||||||
|
func (v *spectrumSpeed) SetValue(value int) bool {
|
||||||
|
v.specAnSettings.Smooth = value
|
||||||
|
TrySend(v.broker.ToSpecAn, MsgToSpecAn{HasSettings: true, SpecSettings: v.specAnSettings})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (v *spectrumSpeed) Range() RangeInclusive { return RangeInclusive{-3, 3} }
|
||||||
|
|
||||||
|
const (
|
||||||
|
SpecSpeedMin = -3
|
||||||
|
SpecSpeedMax = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Resolution returns an Int to adjust the resolution of the spectrum analyzer.
|
||||||
|
func (m *SpectrumModel) Resolution() Int { return MakeInt((*spectrumResolution)(m)) }
|
||||||
|
|
||||||
|
type spectrumResolution Model
|
||||||
|
|
||||||
|
func (v *spectrumResolution) Value() int { return v.specAnSettings.Resolution }
|
||||||
|
func (v *spectrumResolution) SetValue(value int) bool {
|
||||||
|
v.specAnSettings.Resolution = value
|
||||||
|
TrySend(v.broker.ToSpecAn, MsgToSpecAn{HasSettings: true, SpecSettings: v.specAnSettings})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (v *spectrumResolution) Range() RangeInclusive {
|
||||||
|
return RangeInclusive{SpecResolutionMin, SpecResolutionMax}
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SpecResolutionMin = -3
|
SpecResolutionMin = -3
|
||||||
SpecResolutionMax = 3
|
SpecResolutionMax = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// Channels returns an Int to adjust the channel mode of the spectrum analyzer.
|
||||||
SpecSpeedMin = -3
|
func (m *SpectrumModel) Channels() Int { return MakeInt((*spectrumChannels)(m)) }
|
||||||
SpecSpeedMax = 3
|
|
||||||
)
|
type spectrumChannels Model
|
||||||
|
|
||||||
|
func (v *spectrumChannels) Value() int { return int(v.specAnSettings.ChnMode) }
|
||||||
|
func (v *spectrumChannels) SetValue(value int) bool {
|
||||||
|
v.specAnSettings.ChnMode = SpecChnMode(value)
|
||||||
|
TrySend(v.broker.ToSpecAn, MsgToSpecAn{HasSettings: true, SpecSettings: v.specAnSettings})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (v *spectrumChannels) Range() RangeInclusive {
|
||||||
|
return RangeInclusive{0, int(NumSpecChnModes) - 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpecChnMode int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SpecChnModeSum SpecChnMode = iota // calculate a single combined spectrum for both channels
|
SpecChnModeSum SpecChnMode = iota // calculate a single combined spectrum for both channels
|
||||||
@@ -58,15 +80,14 @@ const (
|
|||||||
NumSpecChnModes
|
NumSpecChnModes
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Model) SpecAnEnabled() Bool { return MakeBoolFromPtr(&m.specAnEnabled) }
|
// Enabled returns a Bool to toggle whether the spectrum analyzer is enabled or
|
||||||
|
// not. If it is disabled, it will not process any audio data, saving CPU
|
||||||
|
// resources.
|
||||||
|
func (m *SpectrumModel) Enabled() Bool { return MakeBoolFromPtr(&m.specAnEnabled) }
|
||||||
|
|
||||||
func NewSpecAnalyzer(broker *Broker) *SpecAnalyzer {
|
// BiquadCoeffs returns the biquad filter coefficients of the currently selected
|
||||||
ret := &SpecAnalyzer{broker: broker}
|
// filter or belleq, to plot its frequency response on top of the spectrum.
|
||||||
ret.init(SpecAnSettings{})
|
func (m *SpectrumModel) BiquadCoeffs() (coeffs BiquadCoeffs, ok bool) {
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) BiquadCoeffs() (coeffs BiquadCoeffs, ok bool) {
|
|
||||||
i := m.d.InstrIndex
|
i := m.d.InstrIndex
|
||||||
u := m.d.UnitIndex
|
u := m.d.UnitIndex
|
||||||
if i < 0 || i >= len(m.d.Song.Patch) || u < 0 || u >= len(m.d.Song.Patch[i].Units) {
|
if i < 0 || i >= len(m.d.Song.Patch) || u < 0 || u >= len(m.d.Song.Patch[i].Units) {
|
||||||
@@ -128,13 +149,44 @@ func (m *Model) BiquadCoeffs() (coeffs BiquadCoeffs, ok bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BiquadCoeffs struct {
|
||||||
|
b0, b1, b2 float32
|
||||||
|
a0, a1, a2 float32
|
||||||
|
}
|
||||||
|
|
||||||
func (c *BiquadCoeffs) Gain(omega float32) float32 {
|
func (c *BiquadCoeffs) Gain(omega float32) float32 {
|
||||||
e := cmplx.Rect(1, -float64(omega))
|
e := cmplx.Rect(1, -float64(omega))
|
||||||
return float32(cmplx.Abs((complex(float64(c.b0), 0) + complex(float64(c.b1), 0)*e + complex(float64(c.b2), 0)*(e*e)) /
|
return float32(cmplx.Abs((complex(float64(c.b0), 0) + complex(float64(c.b1), 0)*e + complex(float64(c.b2), 0)*(e*e)) /
|
||||||
(complex(float64(c.a0), 0) + complex(float64(c.a1), 0)*e + complex(float64(c.a2), 0)*e*e)))
|
(complex(float64(c.a0), 0) + complex(float64(c.a1), 0)*e + complex(float64(c.a2), 0)*e*e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SpecAnalyzer) Run() {
|
type (
|
||||||
|
specAnalyzer struct {
|
||||||
|
settings specAnSettings
|
||||||
|
broker *Broker
|
||||||
|
chunker chunker
|
||||||
|
temp specTemp
|
||||||
|
}
|
||||||
|
|
||||||
|
specAnSettings struct {
|
||||||
|
ChnMode SpecChnMode
|
||||||
|
Smooth int
|
||||||
|
Resolution int
|
||||||
|
}
|
||||||
|
|
||||||
|
specTemp struct {
|
||||||
|
power [2][]float32
|
||||||
|
window []float32 // window weighting function
|
||||||
|
normFactor float32 // normalization factor, to account for the windowing
|
||||||
|
bitPerm []int // bit-reversal permutation table
|
||||||
|
tmpC []complex128 // temporary buffer for FFT
|
||||||
|
tmp1, tmp2 []float32 // temporary buffers for processing
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func runSpecAnalyzer(broker *Broker) {
|
||||||
|
s := &specAnalyzer{broker: broker}
|
||||||
|
s.init(specAnSettings{})
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-s.broker.CloseSpecAn:
|
case <-s.broker.CloseSpecAn:
|
||||||
@@ -146,7 +198,7 @@ func (s *SpecAnalyzer) Run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SpecAnalyzer) handleMsg(msg MsgToSpecAn) {
|
func (s *specAnalyzer) handleMsg(msg MsgToSpecAn) {
|
||||||
if msg.HasSettings {
|
if msg.HasSettings {
|
||||||
s.init(msg.SpecSettings)
|
s.init(msg.SpecSettings)
|
||||||
}
|
}
|
||||||
@@ -164,7 +216,7 @@ func (s *SpecAnalyzer) handleMsg(msg MsgToSpecAn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SpecAnalyzer) init(s SpecAnSettings) {
|
func (a *specAnalyzer) init(s specAnSettings) {
|
||||||
s.Resolution = min(max(s.Resolution, SpecResolutionMin), SpecResolutionMax) + 10
|
s.Resolution = min(max(s.Resolution, SpecResolutionMin), SpecResolutionMax) + 10
|
||||||
a.settings = s
|
a.settings = s
|
||||||
n := 1 << s.Resolution
|
n := 1 << s.Resolution
|
||||||
@@ -198,7 +250,7 @@ func (a *SpecAnalyzer) init(s SpecAnSettings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SpecAnalyzer) update(buf sointu.AudioBuffer) *Spectrum {
|
func (s *specAnalyzer) update(buf sointu.AudioBuffer) *Spectrum {
|
||||||
ret := s.broker.GetSpectrum()
|
ret := s.broker.GetSpectrum()
|
||||||
switch s.settings.ChnMode {
|
switch s.settings.ChnMode {
|
||||||
case SpecChnModeSeparate:
|
case SpecChnModeSeparate:
|
||||||
@@ -220,7 +272,7 @@ func (s *SpecAnalyzer) update(buf sointu.AudioBuffer) *Spectrum {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sd *SpecAnalyzer) process(buf sointu.AudioBuffer, channel int) {
|
func (sd *specAnalyzer) process(buf sointu.AudioBuffer, channel int) {
|
||||||
for i := range buf { // de-interleave
|
for i := range buf { // de-interleave
|
||||||
sd.temp.tmp1[i] = removeNaNsAndClamp(buf[i][channel])
|
sd.temp.tmp1[i] = removeNaNsAndClamp(buf[i][channel])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
package tracker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
String struct {
|
|
||||||
value StringValue
|
|
||||||
}
|
|
||||||
|
|
||||||
StringValue interface {
|
|
||||||
Value() string
|
|
||||||
SetValue(string) bool
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func MakeString(value StringValue) String {
|
|
||||||
return String{value: value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v String) SetValue(value string) bool {
|
|
||||||
if v.value == nil || v.value.Value() == value {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return v.value.SetValue(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v String) Value() string {
|
|
||||||
if v.value == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return v.value.Value()
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilePathString
|
|
||||||
type filePath Model
|
|
||||||
|
|
||||||
func (m *Model) FilePath() String { return MakeString((*filePath)(m)) }
|
|
||||||
func (v *filePath) Value() string { return v.d.FilePath }
|
|
||||||
func (v *filePath) SetValue(value string) bool { v.d.FilePath = value; return true }
|
|
||||||
|
|
||||||
// UnitSearchString
|
|
||||||
type unitSearch Model
|
|
||||||
|
|
||||||
func (m *Model) UnitSearch() String { return MakeString((*unitSearch)(m)) }
|
|
||||||
func (v *unitSearch) Value() string {
|
|
||||||
// return current unit type string if not searching
|
|
||||||
if !v.d.UnitSearching {
|
|
||||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if v.d.UnitIndex < 0 || v.d.UnitIndex >= len(v.d.Song.Patch[v.d.InstrIndex].Units) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return v.d.Song.Patch[v.d.InstrIndex].Units[v.d.UnitIndex].Type
|
|
||||||
} else {
|
|
||||||
return v.d.UnitSearchString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (v *unitSearch) SetValue(value string) bool {
|
|
||||||
v.d.UnitSearchString = value
|
|
||||||
v.d.UnitSearching = true
|
|
||||||
(*Model)(v).updateDerivedUnitSearch()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
func (v *Model) updateDerivedUnitSearch() {
|
|
||||||
// update search results based on current search string
|
|
||||||
v.derived.searchResults = v.derived.searchResults[:0]
|
|
||||||
for _, name := range sointu.UnitNames {
|
|
||||||
if strings.HasPrefix(name, v.UnitSearch().Value()) {
|
|
||||||
v.derived.searchResults = append(v.derived.searchResults, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstrumentNameString
|
|
||||||
type instrumentName Model
|
|
||||||
|
|
||||||
func (m *Model) InstrumentName() String { return MakeString((*instrumentName)(m)) }
|
|
||||||
func (v *instrumentName) Value() string {
|
|
||||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return v.d.Song.Patch[v.d.InstrIndex].Name
|
|
||||||
}
|
|
||||||
func (v *instrumentName) SetValue(value string) bool {
|
|
||||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer (*Model)(v).change("InstrumentNameString", PatchChange, MinorChange)()
|
|
||||||
v.d.Song.Patch[v.d.InstrIndex].Name = value
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstrumentComment
|
|
||||||
type instrumentComment Model
|
|
||||||
|
|
||||||
func (m *Model) InstrumentComment() String { return MakeString((*instrumentComment)(m)) }
|
|
||||||
func (v *instrumentComment) Value() string {
|
|
||||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return v.d.Song.Patch[v.d.InstrIndex].Comment
|
|
||||||
}
|
|
||||||
func (v *instrumentComment) SetValue(value string) bool {
|
|
||||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer (*Model)(v).change("InstrumentComment", PatchChange, MinorChange)()
|
|
||||||
v.d.Song.Patch[v.d.InstrIndex].Comment = value
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnitComment
|
|
||||||
type unitComment Model
|
|
||||||
|
|
||||||
func (m *Model) UnitComment() String { return MakeString((*unitComment)(m)) }
|
|
||||||
func (v *unitComment) Value() string {
|
|
||||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) ||
|
|
||||||
v.d.UnitIndex < 0 || v.d.UnitIndex >= len(v.d.Song.Patch[v.d.InstrIndex].Units) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return v.d.Song.Patch[v.d.InstrIndex].Units[v.d.UnitIndex].Comment
|
|
||||||
}
|
|
||||||
func (v *unitComment) SetValue(value string) bool {
|
|
||||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) ||
|
|
||||||
v.d.UnitIndex < 0 || v.d.UnitIndex >= len(v.d.Song.Patch[v.d.InstrIndex].Units) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer (*Model)(v).change("UnitComment", PatchChange, MinorChange)()
|
|
||||||
v.d.Song.Patch[v.d.InstrIndex].Units[v.d.UnitIndex].Comment = value
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
625
tracker/table.go
625
tracker/table.go
@@ -1,625 +0,0 @@
|
|||||||
package tracker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Table struct {
|
|
||||||
TableData
|
|
||||||
}
|
|
||||||
|
|
||||||
TableData interface {
|
|
||||||
Cursor() Point
|
|
||||||
Cursor2() Point
|
|
||||||
SetCursor(Point)
|
|
||||||
SetCursor2(Point)
|
|
||||||
Width() int
|
|
||||||
Height() int
|
|
||||||
MoveCursor(dx, dy int) (ok bool)
|
|
||||||
|
|
||||||
clear(p Point)
|
|
||||||
set(p Point, value int)
|
|
||||||
add(rect Rect, delta int, largestep bool) (ok bool)
|
|
||||||
marshal(rect Rect) (data []byte, ok bool)
|
|
||||||
unmarshalAtCursor(data []byte) (ok bool)
|
|
||||||
unmarshalRange(rect Rect, data []byte) (ok bool)
|
|
||||||
change(kind string, severity ChangeSeverity) func()
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
Point struct {
|
|
||||||
X, Y int
|
|
||||||
}
|
|
||||||
|
|
||||||
Rect struct {
|
|
||||||
TopLeft, BottomRight Point
|
|
||||||
}
|
|
||||||
|
|
||||||
Order Model
|
|
||||||
Notes Model
|
|
||||||
)
|
|
||||||
|
|
||||||
// Model methods
|
|
||||||
|
|
||||||
func (m *Model) Order() *Order { return (*Order)(m) }
|
|
||||||
func (m *Model) Notes() *Notes { return (*Notes)(m) }
|
|
||||||
|
|
||||||
// Rect methods
|
|
||||||
|
|
||||||
func (r *Rect) Contains(p Point) bool {
|
|
||||||
return r.TopLeft.X <= p.X && p.X <= r.BottomRight.X &&
|
|
||||||
r.TopLeft.Y <= p.Y && p.Y <= r.BottomRight.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Rect) Width() int {
|
|
||||||
return r.BottomRight.X - r.TopLeft.X + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Rect) Height() int {
|
|
||||||
return r.BottomRight.Y - r.TopLeft.Y + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Rect) Limit(width, height int) {
|
|
||||||
if r.TopLeft.X < 0 {
|
|
||||||
r.TopLeft.X = 0
|
|
||||||
}
|
|
||||||
if r.TopLeft.Y < 0 {
|
|
||||||
r.TopLeft.Y = 0
|
|
||||||
}
|
|
||||||
if r.BottomRight.X >= width {
|
|
||||||
r.BottomRight.X = width - 1
|
|
||||||
}
|
|
||||||
if r.BottomRight.Y >= height {
|
|
||||||
r.BottomRight.Y = height - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Table methods
|
|
||||||
|
|
||||||
func (v Table) Range() (rect Rect) {
|
|
||||||
rect.TopLeft.X = min(v.Cursor().X, v.Cursor2().X)
|
|
||||||
rect.TopLeft.Y = min(v.Cursor().Y, v.Cursor2().Y)
|
|
||||||
rect.BottomRight.X = max(v.Cursor().X, v.Cursor2().X)
|
|
||||||
rect.BottomRight.Y = max(v.Cursor().Y, v.Cursor2().Y)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Table) Copy() ([]byte, bool) {
|
|
||||||
ret, ok := v.marshal(v.Range())
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return ret, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Table) Paste(data []byte) bool {
|
|
||||||
defer v.change("Paste", MajorChange)()
|
|
||||||
if v.Cursor() == v.Cursor2() {
|
|
||||||
return v.unmarshalAtCursor(data)
|
|
||||||
} else {
|
|
||||||
return v.unmarshalRange(v.Range(), data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Table) Clear() {
|
|
||||||
defer v.change("Clear", MajorChange)()
|
|
||||||
rect := v.Range()
|
|
||||||
rect.Limit(v.Width(), v.Height())
|
|
||||||
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
|
||||||
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
|
||||||
v.clear(Point{x, y})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Table) Set(value byte) {
|
|
||||||
defer v.change("Set", MajorChange)()
|
|
||||||
cursor := v.Cursor()
|
|
||||||
// TODO: might check for visibility
|
|
||||||
v.set(cursor, int(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Table) Fill(value int) {
|
|
||||||
defer v.change("Fill", MajorChange)()
|
|
||||||
rect := v.Range()
|
|
||||||
rect.Limit(v.Width(), v.Height())
|
|
||||||
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
|
||||||
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
|
||||||
v.set(Point{x, y}, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Table) Add(delta int, largeStep bool) {
|
|
||||||
defer v.change("Add", MinorChange)()
|
|
||||||
if !v.add(v.Range(), delta, largeStep) {
|
|
||||||
v.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Table) SetCursorX(x int) {
|
|
||||||
p := v.Cursor()
|
|
||||||
p.X = x
|
|
||||||
v.SetCursor(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Table) SetCursorY(y int) {
|
|
||||||
p := v.Cursor()
|
|
||||||
p.Y = y
|
|
||||||
v.SetCursor(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order methods
|
|
||||||
|
|
||||||
func (v *Order) Table() Table {
|
|
||||||
return Table{v}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Order) Cursor() Point {
|
|
||||||
t := max(min(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
|
||||||
p := max(min(m.d.Cursor.OrderRow, m.d.Song.Score.Length-1), 0)
|
|
||||||
return Point{t, p}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Order) Cursor2() Point {
|
|
||||||
t := max(min(m.d.Cursor2.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
|
||||||
p := max(min(m.d.Cursor2.OrderRow, m.d.Song.Score.Length-1), 0)
|
|
||||||
return Point{t, p}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Order) SetCursor(p Point) {
|
|
||||||
m.d.Cursor.Track = max(min(p.X, len(m.d.Song.Score.Tracks)-1), 0)
|
|
||||||
y := max(min(p.Y, m.d.Song.Score.Length-1), 0)
|
|
||||||
if y != m.d.Cursor.OrderRow {
|
|
||||||
m.follow = false
|
|
||||||
}
|
|
||||||
m.d.Cursor.OrderRow = y
|
|
||||||
m.updateCursorRows()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Order) SetCursor2(p Point) {
|
|
||||||
m.d.Cursor2.Track = max(min(p.X, len(m.d.Song.Score.Tracks)-1), 0)
|
|
||||||
m.d.Cursor2.OrderRow = max(min(p.Y, m.d.Song.Score.Length-1), 0)
|
|
||||||
m.updateCursorRows()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Order) updateCursorRows() {
|
|
||||||
if v.Cursor() == v.Cursor2() {
|
|
||||||
v.d.Cursor.PatternRow = 0
|
|
||||||
v.d.Cursor2.PatternRow = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if v.d.Cursor.OrderRow > v.d.Cursor2.OrderRow {
|
|
||||||
v.d.Cursor.PatternRow = v.d.Song.Score.RowsPerPattern - 1
|
|
||||||
v.d.Cursor2.PatternRow = 0
|
|
||||||
} else {
|
|
||||||
v.d.Cursor.PatternRow = 0
|
|
||||||
v.d.Cursor2.PatternRow = v.d.Song.Score.RowsPerPattern - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Order) Width() int {
|
|
||||||
return len((*Model)(v).d.Song.Score.Tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Order) Height() int {
|
|
||||||
return (*Model)(v).d.Song.Score.Length
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Order) MoveCursor(dx, dy int) (ok bool) {
|
|
||||||
p := v.Cursor()
|
|
||||||
p.X += dx
|
|
||||||
p.Y += dy
|
|
||||||
v.SetCursor(p)
|
|
||||||
return p == v.Cursor()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Order) clear(p Point) {
|
|
||||||
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Order) set(p Point, value int) {
|
|
||||||
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Order) add(rect Rect, delta int, largeStep bool) (ok bool) {
|
|
||||||
if largeStep {
|
|
||||||
delta *= 8
|
|
||||||
}
|
|
||||||
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
|
||||||
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
|
||||||
if !v.add1(Point{x, y}, delta) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Order) add1(p Point, delta int) (ok bool) {
|
|
||||||
if p.X < 0 || p.X >= len(v.d.Song.Score.Tracks) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val := v.d.Song.Score.Tracks[p.X].Order.Get(p.Y)
|
|
||||||
if val < 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val += delta
|
|
||||||
if val < 0 || val > 36 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
v.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type marshalOrder struct {
|
|
||||||
Order []int `yaml:",flow"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type marshalTracks struct {
|
|
||||||
Tracks []marshalOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Order) marshal(rect Rect) (data []byte, ok bool) {
|
|
||||||
width := rect.BottomRight.X - rect.TopLeft.X + 1
|
|
||||||
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
|
|
||||||
var table = marshalTracks{Tracks: make([]marshalOrder, 0, width)}
|
|
||||||
for x := 0; x < width; x++ {
|
|
||||||
ax := x + rect.TopLeft.X
|
|
||||||
if ax < 0 || ax >= len(m.d.Song.Score.Tracks) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
table.Tracks = append(table.Tracks, marshalOrder{Order: make([]int, 0, rect.BottomRight.Y-rect.TopLeft.Y+1)})
|
|
||||||
for y := 0; y < height; y++ {
|
|
||||||
table.Tracks[x].Order = append(table.Tracks[x].Order, m.d.Song.Score.Tracks[ax].Order.Get(y+rect.TopLeft.Y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret, err := yaml.Marshal(table)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return ret, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Order) unmarshal(data []byte) (marshalTracks, bool) {
|
|
||||||
var table marshalTracks
|
|
||||||
yaml.Unmarshal(data, &table)
|
|
||||||
if len(table.Tracks) == 0 {
|
|
||||||
return marshalTracks{}, false
|
|
||||||
}
|
|
||||||
for i := 0; i < len(table.Tracks); i++ {
|
|
||||||
if len(table.Tracks[i].Order) > 0 {
|
|
||||||
return table, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return marshalTracks{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Order) unmarshalAtCursor(data []byte) bool {
|
|
||||||
table, ok := v.unmarshal(data)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := 0; i < len(table.Tracks); i++ {
|
|
||||||
for j, q := range table.Tracks[i].Order {
|
|
||||||
if table.Tracks[i].Order[j] < -1 || table.Tracks[i].Order[j] > 36 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
x := i + v.Cursor().X
|
|
||||||
y := j + v.Cursor().Y
|
|
||||||
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.Length {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
v.d.Song.Score.Tracks[x].Order.Set(y, q)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Order) unmarshalRange(rect Rect, data []byte) bool {
|
|
||||||
table, ok := v.unmarshal(data)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := 0; i < rect.Width(); i++ {
|
|
||||||
for j := 0; j < rect.Height(); j++ {
|
|
||||||
k := i % len(table.Tracks)
|
|
||||||
l := j % len(table.Tracks[k].Order)
|
|
||||||
a := table.Tracks[k].Order[l]
|
|
||||||
if a < -1 || a > 36 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
x := i + rect.TopLeft.X
|
|
||||||
y := j + rect.TopLeft.Y
|
|
||||||
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.Length {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
v.d.Song.Score.Tracks[x].Order.Set(y, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Order) change(kind string, severity ChangeSeverity) func() {
|
|
||||||
return (*Model)(v).change("OrderTableView."+kind, ScoreChange, severity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Order) cancel() {
|
|
||||||
v.changeCancel = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Order) Value(p Point) int {
|
|
||||||
if p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return m.d.Song.Score.Tracks[p.X].Order.Get(p.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Order) SetValue(p Point, val int) {
|
|
||||||
defer (*Model)(m).change("OrderElement.SetValue", ScoreChange, MinorChange)()
|
|
||||||
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoteTable
|
|
||||||
|
|
||||||
func (v *Notes) Table() Table {
|
|
||||||
return Table{v}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Notes) Cursor() Point {
|
|
||||||
t := max(min(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
|
||||||
p := max(min(m.d.Song.Score.SongRow(m.d.Cursor.SongPos), m.d.Song.Score.LengthInRows()-1), 0)
|
|
||||||
return Point{t, p}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Notes) Cursor2() Point {
|
|
||||||
t := max(min(m.d.Cursor2.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
|
||||||
p := max(min(m.d.Song.Score.SongRow(m.d.Cursor2.SongPos), m.d.Song.Score.LengthInRows()-1), 0)
|
|
||||||
return Point{t, p}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) SetCursor(p Point) {
|
|
||||||
v.d.Cursor.Track = max(min(p.X, len(v.d.Song.Score.Tracks)-1), 0)
|
|
||||||
newPos := v.d.Song.Score.Clamp(sointu.SongPos{PatternRow: p.Y})
|
|
||||||
if newPos != v.d.Cursor.SongPos {
|
|
||||||
v.follow = false
|
|
||||||
}
|
|
||||||
v.d.Cursor.SongPos = newPos
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) SetCursor2(p Point) {
|
|
||||||
v.d.Cursor2.Track = max(min(p.X, len(v.d.Song.Score.Tracks)-1), 0)
|
|
||||||
v.d.Cursor2.SongPos = v.d.Song.Score.Clamp(sointu.SongPos{PatternRow: p.Y})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Notes) SetCursorFloat(x, y float32) {
|
|
||||||
m.SetCursor(Point{int(x), int(y)})
|
|
||||||
m.d.LowNibble = math.Mod(float64(x), 1.0) > 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) Width() int {
|
|
||||||
return len((*Model)(v).d.Song.Score.Tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) Height() int {
|
|
||||||
return (*Model)(v).d.Song.Score.Length * (*Model)(v).d.Song.Score.RowsPerPattern
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) MoveCursor(dx, dy int) (ok bool) {
|
|
||||||
p := v.Cursor()
|
|
||||||
for dx < 0 {
|
|
||||||
if v.Effect(p.X) && v.d.LowNibble {
|
|
||||||
v.d.LowNibble = false
|
|
||||||
} else {
|
|
||||||
p.X--
|
|
||||||
v.d.LowNibble = true
|
|
||||||
}
|
|
||||||
dx++
|
|
||||||
}
|
|
||||||
for dx > 0 {
|
|
||||||
if v.Effect(p.X) && !v.d.LowNibble {
|
|
||||||
v.d.LowNibble = true
|
|
||||||
} else {
|
|
||||||
p.X++
|
|
||||||
v.d.LowNibble = false
|
|
||||||
}
|
|
||||||
dx--
|
|
||||||
}
|
|
||||||
p.Y += dy
|
|
||||||
v.SetCursor(p)
|
|
||||||
return p == v.Cursor()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) clear(p Point) {
|
|
||||||
v.Input(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) set(p Point, value int) {
|
|
||||||
v.SetValue(p, byte(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) add(rect Rect, delta int, largeStep bool) (ok bool) {
|
|
||||||
if largeStep {
|
|
||||||
delta *= 12
|
|
||||||
}
|
|
||||||
for x := rect.BottomRight.X; x >= rect.TopLeft.X; x-- {
|
|
||||||
for y := rect.BottomRight.Y; y >= rect.TopLeft.Y; y-- {
|
|
||||||
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pos := v.d.Song.Score.SongPos(y)
|
|
||||||
note := v.d.Song.Score.Tracks[x].Note(pos)
|
|
||||||
if note <= 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newVal := int(note) + delta
|
|
||||||
if newVal < 2 {
|
|
||||||
newVal = 2
|
|
||||||
} else if newVal > 255 {
|
|
||||||
newVal = 255
|
|
||||||
}
|
|
||||||
// only do all sets after all gets, so we don't accidentally adjust single note multiple times
|
|
||||||
defer v.d.Song.Score.Tracks[x].SetNote(pos, byte(newVal), v.uniquePatterns)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type noteTable struct {
|
|
||||||
Notes [][]byte `yaml:",flow"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Notes) marshal(rect Rect) (data []byte, ok bool) {
|
|
||||||
width := rect.BottomRight.X - rect.TopLeft.X + 1
|
|
||||||
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
|
|
||||||
var table = noteTable{Notes: make([][]byte, 0, width)}
|
|
||||||
for x := 0; x < width; x++ {
|
|
||||||
table.Notes = append(table.Notes, make([]byte, 0, rect.BottomRight.Y-rect.TopLeft.Y+1))
|
|
||||||
for y := 0; y < height; y++ {
|
|
||||||
pos := m.d.Song.Score.SongPos(y + rect.TopLeft.Y)
|
|
||||||
ax := x + rect.TopLeft.X
|
|
||||||
if ax < 0 || ax >= len(m.d.Song.Score.Tracks) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
table.Notes[x] = append(table.Notes[x], m.d.Song.Score.Tracks[ax].Note(pos))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret, err := yaml.Marshal(table)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return ret, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) unmarshal(data []byte) (noteTable, bool) {
|
|
||||||
var table noteTable
|
|
||||||
yaml.Unmarshal(data, &table)
|
|
||||||
if len(table.Notes) == 0 {
|
|
||||||
return noteTable{}, false
|
|
||||||
}
|
|
||||||
for i := 0; i < len(table.Notes); i++ {
|
|
||||||
if len(table.Notes[i]) > 0 {
|
|
||||||
return table, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return noteTable{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) unmarshalAtCursor(data []byte) bool {
|
|
||||||
table, ok := v.unmarshal(data)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := 0; i < len(table.Notes); i++ {
|
|
||||||
for j, q := range table.Notes[i] {
|
|
||||||
x := i + v.Cursor().X
|
|
||||||
y := j + v.Cursor().Y
|
|
||||||
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pos := v.d.Song.Score.SongPos(y)
|
|
||||||
v.d.Song.Score.Tracks[x].SetNote(pos, q, v.uniquePatterns)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) unmarshalRange(rect Rect, data []byte) bool {
|
|
||||||
table, ok := v.unmarshal(data)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := 0; i < rect.Width(); i++ {
|
|
||||||
for j := 0; j < rect.Height(); j++ {
|
|
||||||
k := i % len(table.Notes)
|
|
||||||
l := j % len(table.Notes[k])
|
|
||||||
a := table.Notes[k][l]
|
|
||||||
x := i + rect.TopLeft.X
|
|
||||||
y := j + rect.TopLeft.Y
|
|
||||||
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pos := v.d.Song.Score.SongPos(y)
|
|
||||||
v.d.Song.Score.Tracks[x].SetNote(pos, a, v.uniquePatterns)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) change(kind string, severity ChangeSeverity) func() {
|
|
||||||
return (*Model)(v).change("OrderTableView."+kind, ScoreChange, severity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) cancel() {
|
|
||||||
v.changeCancel = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Notes) Value(p Point) byte {
|
|
||||||
if p.Y < 0 || p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
pos := m.d.Song.Score.SongPos(p.Y)
|
|
||||||
return m.d.Song.Score.Tracks[p.X].Note(pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Notes) Effect(x int) bool {
|
|
||||||
if x < 0 || x >= len(m.d.Song.Score.Tracks) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return m.d.Song.Score.Tracks[x].Effect
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Notes) LowNibble() bool {
|
|
||||||
return m.d.LowNibble
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Notes) SetValue(p Point, val byte) {
|
|
||||||
defer m.change("SetValue", MinorChange)()
|
|
||||||
if p.Y < 0 || p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
track := &(m.d.Song.Score.Tracks[p.X])
|
|
||||||
pos := m.d.Song.Score.SongPos(p.Y)
|
|
||||||
(*track).SetNote(pos, val, m.uniquePatterns)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) Input(note byte) NoteEvent {
|
|
||||||
v.Table().Fill(int(note))
|
|
||||||
return v.finishInput(note)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) InputNibble(nibble byte) NoteEvent {
|
|
||||||
defer v.change("FillNibble", MajorChange)()
|
|
||||||
rect := Table{v}.Range()
|
|
||||||
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
|
|
||||||
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
|
|
||||||
val := v.Value(Point{x, y})
|
|
||||||
if val == 1 {
|
|
||||||
val = 0 // treat hold also as 0
|
|
||||||
}
|
|
||||||
if v.d.LowNibble {
|
|
||||||
val = (val & 0xf0) | byte(nibble&15)
|
|
||||||
} else {
|
|
||||||
val = (val & 0x0f) | byte((nibble&15)<<4)
|
|
||||||
}
|
|
||||||
v.SetValue(Point{x, y}, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v.finishInput(v.Value(v.Cursor()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Notes) finishInput(note byte) NoteEvent {
|
|
||||||
if step := v.d.Step; step > 0 {
|
|
||||||
v.Table().MoveCursor(0, step)
|
|
||||||
v.Table().SetCursor2(v.Table().Cursor())
|
|
||||||
}
|
|
||||||
TrySend(v.broker.ToGUI, any(MsgToGUI{Kind: GUIMessageEnsureCursorVisible, Param: v.Table().Cursor().Y}))
|
|
||||||
track := v.Cursor().X
|
|
||||||
ts := time.Now().UnixMilli() * 441 / 10 // convert to 44100Hz frames
|
|
||||||
return NoteEvent{IsTrack: true, Channel: track, Note: note, On: true, Timestamp: ts}
|
|
||||||
}
|
|
||||||
186
tracker/track.go
Normal file
186
tracker/track.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
|
"github.com/vsariola/sointu/vm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Track returns the Track view of the model, containing methods to manipulate
|
||||||
|
// the tracks.
|
||||||
|
func (m *Model) Track() *TrackModel { return (*TrackModel)(m) }
|
||||||
|
|
||||||
|
type TrackModel Model
|
||||||
|
|
||||||
|
// LinkInstrument returns a Bool controlling whether instruments and tracks are
|
||||||
|
// linked.
|
||||||
|
func (m *TrackModel) LinkInstrument() Bool { return MakeBoolFromPtr(&m.linkInstrTrack) }
|
||||||
|
|
||||||
|
// Title returns the title of the track for a given index.
|
||||||
|
func (m *TrackModel) Item(index int) TrackListItem {
|
||||||
|
if index < 0 || index >= len(m.derived.tracks) {
|
||||||
|
return TrackListItem{}
|
||||||
|
}
|
||||||
|
return TrackListItem{m.derived.tracks[index].title, m.d.Song.Score.Tracks[index].Effect}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrackListItem struct {
|
||||||
|
Title string
|
||||||
|
Effect bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add returns an Action to add a new track.
|
||||||
|
func (m *TrackModel) Add() Action { return MakeAction((*addTrack)(m)) }
|
||||||
|
|
||||||
|
type addTrack TrackModel
|
||||||
|
|
||||||
|
func (m *addTrack) Enabled() bool { return m.d.Song.Score.NumVoices() < vm.MAX_VOICES }
|
||||||
|
func (m *addTrack) Do() {
|
||||||
|
defer (*Model)(m).change("AddTrack", SongChange, MajorChange)()
|
||||||
|
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
|
||||||
|
p := sointu.Patch{defaultInstrument.Copy()}
|
||||||
|
t := []sointu.Track{{NumVoices: 1}}
|
||||||
|
_, _, ok := (*Model)(m).addVoices(voiceIndex, p, t, (*Model)(m).linkInstrTrack, true)
|
||||||
|
m.changeCancel = !ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete returns an Action to delete the selected track(s).
|
||||||
|
func (m *TrackModel) Delete() Action { return MakeAction((*deleteTrack)(m)) }
|
||||||
|
|
||||||
|
type deleteTrack TrackModel
|
||||||
|
|
||||||
|
func (m *deleteTrack) Enabled() bool { return len(m.d.Song.Score.Tracks) > 0 }
|
||||||
|
func (m *deleteTrack) Do() { (*TrackModel)(m).List().DeleteElements(false) }
|
||||||
|
|
||||||
|
// Split returns an Action to split the selected track into two tracks,
|
||||||
|
// distributing the voices as evenly as possible.
|
||||||
|
func (m *TrackModel) Split() Action { return MakeAction((*splitTrack)(m)) }
|
||||||
|
|
||||||
|
type splitTrack TrackModel
|
||||||
|
|
||||||
|
func (m *splitTrack) Enabled() bool {
|
||||||
|
return m.d.Cursor.Track >= 0 && m.d.Cursor.Track < len(m.d.Song.Score.Tracks) && m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices > 1
|
||||||
|
}
|
||||||
|
func (m *splitTrack) Do() {
|
||||||
|
defer (*Model)(m).change("SplitTrack", SongChange, MajorChange)()
|
||||||
|
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
|
||||||
|
middle := voiceIndex + (m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices+1)/2
|
||||||
|
end := voiceIndex + m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices
|
||||||
|
left, ok := VoiceSlice(m.d.Song.Score.Tracks, Range{math.MinInt, middle})
|
||||||
|
if !ok {
|
||||||
|
m.changeCancel = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
right, ok := VoiceSlice(m.d.Song.Score.Tracks, Range{end, math.MaxInt})
|
||||||
|
if !ok {
|
||||||
|
m.changeCancel = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newTrack := sointu.Track{NumVoices: end - middle}
|
||||||
|
m.d.Song.Score.Tracks = append(left, newTrack)
|
||||||
|
m.d.Song.Score.Tracks = append(m.d.Song.Score.Tracks, right...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effect returns a Bool to toggle whether the currently selected track is an
|
||||||
|
// effect track and should be displayed as hexadecimals or not.
|
||||||
|
func (m *TrackModel) Effect() Bool { return MakeBool((*trackEffect)(m)) }
|
||||||
|
|
||||||
|
type trackEffect TrackModel
|
||||||
|
|
||||||
|
func (m *trackEffect) Value() bool {
|
||||||
|
if m.d.Cursor.Track < 0 || m.d.Cursor.Track >= len(m.d.Song.Score.Tracks) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.d.Song.Score.Tracks[m.d.Cursor.Track].Effect
|
||||||
|
}
|
||||||
|
func (m *trackEffect) SetValue(val bool) {
|
||||||
|
if m.d.Cursor.Track < 0 || m.d.Cursor.Track >= len(m.d.Song.Score.Tracks) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.d.Song.Score.Tracks[m.d.Cursor.Track].Effect = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Voices returns an Int to adjust the number of voices for the currently
|
||||||
|
// selected track.
|
||||||
|
func (m *TrackModel) Voices() Int { return MakeInt((*trackVoices)(m)) }
|
||||||
|
|
||||||
|
type trackVoices TrackModel
|
||||||
|
|
||||||
|
func (v *trackVoices) Value() int {
|
||||||
|
t := v.d.Cursor.Track
|
||||||
|
if t < 0 || t >= len(v.d.Song.Score.Tracks) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return max(v.d.Song.Score.Tracks[t].NumVoices, 1)
|
||||||
|
}
|
||||||
|
func (m *trackVoices) SetValue(value int) bool {
|
||||||
|
defer (*Model)(m).change("TrackVoices", SongChange, MinorChange)()
|
||||||
|
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
|
||||||
|
voiceRange := Range{voiceIndex, voiceIndex + m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices}
|
||||||
|
ranges := MakeSetLength(voiceRange, value)
|
||||||
|
ok := (*Model)(m).sliceInstrumentsTracks(m.linkInstrTrack, true, ranges...)
|
||||||
|
if !ok {
|
||||||
|
m.changeCancel = true
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
func (v *trackVoices) Range() RangeInclusive {
|
||||||
|
t := v.d.Cursor.Track
|
||||||
|
if t < 0 || t >= len(v.d.Song.Score.Tracks) {
|
||||||
|
return RangeInclusive{1, 1}
|
||||||
|
}
|
||||||
|
return RangeInclusive{1, (*Model)(v).remainingVoices(v.linkInstrTrack, true) + v.d.Song.Score.Tracks[t].NumVoices}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns a List of all the tracks, implementing MutableListData
|
||||||
|
func (m *TrackModel) List() List { return List{(*trackList)(m)} }
|
||||||
|
|
||||||
|
type trackList TrackModel
|
||||||
|
|
||||||
|
func (v *trackList) Selected() int { return v.d.Cursor.Track }
|
||||||
|
func (v *trackList) Selected2() int { return v.d.Cursor2.Track }
|
||||||
|
func (v *trackList) SetSelected(value int) { v.d.Cursor.Track = value }
|
||||||
|
func (v *trackList) SetSelected2(value int) { v.d.Cursor2.Track = value }
|
||||||
|
func (v *trackList) Count() int { return len((*Model)(v).d.Song.Score.Tracks) }
|
||||||
|
|
||||||
|
func (v *trackList) Move(r Range, delta int) (ok bool) {
|
||||||
|
voiceDelta := 0
|
||||||
|
if delta < 0 {
|
||||||
|
voiceDelta = -VoiceRange(v.d.Song.Score.Tracks, Range{r.Start + delta, r.Start}).Len()
|
||||||
|
} else if delta > 0 {
|
||||||
|
voiceDelta = VoiceRange(v.d.Song.Score.Tracks, Range{r.End, r.End + delta}).Len()
|
||||||
|
}
|
||||||
|
if voiceDelta == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ranges := MakeMoveRanges(VoiceRange(v.d.Song.Score.Tracks, r), voiceDelta)
|
||||||
|
return (*Model)(v).sliceInstrumentsTracks(v.linkInstrTrack, true, ranges[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *trackList) Delete(r Range) (ok bool) {
|
||||||
|
ranges := Complement(VoiceRange(v.d.Song.Score.Tracks, r))
|
||||||
|
return (*Model)(v).sliceInstrumentsTracks(v.linkInstrTrack, true, ranges[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *trackList) Change(n string, severity ChangeSeverity) func() {
|
||||||
|
return (*Model)(v).change("TrackList."+n, SongChange, severity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *trackList) Cancel() {
|
||||||
|
v.changeCancel = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *trackList) Marshal(r Range) ([]byte, error) {
|
||||||
|
return (*Model)(v).marshalVoices(VoiceRange(v.d.Song.Score.Tracks, r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *trackList) Unmarshal(data []byte) (r Range, err error) {
|
||||||
|
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
|
||||||
|
_, r, ok := (*Model)(m).unmarshalVoices(voiceIndex, data, m.linkInstrTrack, true)
|
||||||
|
if !ok {
|
||||||
|
return Range{}, fmt.Errorf("unmarshal: unmarshalVoices failed")
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
387
tracker/unit.go
Normal file
387
tracker/unit.go
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unit returns the Unit view of the model, containing methods to manipulate the
|
||||||
|
// units.
|
||||||
|
func (m *Model) Unit() *UnitModel { return (*UnitModel)(m) }
|
||||||
|
|
||||||
|
type UnitModel Model
|
||||||
|
|
||||||
|
// Add returns an Action to add a new unit. If the before parameter is true,
|
||||||
|
// then the new unit is added before the currently selected unit; otherwise,
|
||||||
|
// after.
|
||||||
|
func (m *UnitModel) Add(before bool) Action {
|
||||||
|
return MakeAction(addUnit{Before: before, Model: (*Model)(m)})
|
||||||
|
}
|
||||||
|
|
||||||
|
type addUnit struct {
|
||||||
|
Before bool
|
||||||
|
*Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a addUnit) Do() {
|
||||||
|
m := (*Model)(a.Model)
|
||||||
|
defer m.change("AddUnitAction", PatchChange, MajorChange)()
|
||||||
|
if len(m.d.Song.Patch) == 0 { // no instruments, add one
|
||||||
|
instr := sointu.Instrument{NumVoices: 1}
|
||||||
|
instr.Units = make([]sointu.Unit, 0, 1)
|
||||||
|
m.d.Song.Patch = append(m.d.Song.Patch, instr)
|
||||||
|
m.d.UnitIndex = 0
|
||||||
|
} else {
|
||||||
|
if !a.Before {
|
||||||
|
m.d.UnitIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.d.InstrIndex = max(min(m.d.InstrIndex, len(m.d.Song.Patch)-1), 0)
|
||||||
|
instr := m.d.Song.Patch[m.d.InstrIndex]
|
||||||
|
newUnits := make([]sointu.Unit, len(instr.Units)+1)
|
||||||
|
m.d.UnitIndex = clamp(m.d.UnitIndex, 0, len(newUnits)-1)
|
||||||
|
m.d.UnitIndex2 = m.d.UnitIndex
|
||||||
|
copy(newUnits, instr.Units[:m.d.UnitIndex])
|
||||||
|
copy(newUnits[m.d.UnitIndex+1:], instr.Units[m.d.UnitIndex:])
|
||||||
|
m.assignUnitIDs(newUnits[m.d.UnitIndex : m.d.UnitIndex+1])
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].Units = newUnits
|
||||||
|
m.d.ParamIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete returns an Action to delete the currently selected unit(s).
|
||||||
|
func (m *UnitModel) Delete() Action { return MakeAction((*deleteUnit)(m)) }
|
||||||
|
|
||||||
|
type deleteUnit UnitModel
|
||||||
|
|
||||||
|
func (m *deleteUnit) Enabled() bool {
|
||||||
|
i := (*Model)(m).d.InstrIndex
|
||||||
|
return i >= 0 && i < len((*Model)(m).d.Song.Patch) && len((*Model)(m).d.Song.Patch[i].Units) > 1
|
||||||
|
}
|
||||||
|
func (m *deleteUnit) Do() {
|
||||||
|
defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)()
|
||||||
|
(*UnitModel)(m).List().DeleteElements(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear returns an Action to clear the currently selected unit(s) i.e. they are
|
||||||
|
// set as empty units, but are kept in the unit list.
|
||||||
|
func (m *UnitModel) Clear() Action { return MakeAction((*clearUnit)(m)) }
|
||||||
|
|
||||||
|
type clearUnit UnitModel
|
||||||
|
|
||||||
|
func (m *clearUnit) Enabled() bool {
|
||||||
|
i := (*Model)(m).d.InstrIndex
|
||||||
|
return i >= 0 && i < len(m.d.Song.Patch) && len(m.d.Song.Patch[i].Units) > 0
|
||||||
|
}
|
||||||
|
func (m *clearUnit) Do() {
|
||||||
|
defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)()
|
||||||
|
l := ((*UnitModel)(m)).List()
|
||||||
|
r := l.listRange()
|
||||||
|
for i := r.Start; i < r.End; i++ {
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].Units[i] = sointu.Unit{}
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].Units[i].ID = (*Model)(m).maxID() + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Searching returns a Bool telling whether the user is currently searching for
|
||||||
|
// a unit (should the search resultsbe displayed).
|
||||||
|
func (m *UnitModel) Searching() Bool { return MakeBool((*unitSearching)(m)) }
|
||||||
|
|
||||||
|
type unitSearching UnitModel
|
||||||
|
|
||||||
|
func (m *unitSearching) Value() bool { return m.d.UnitSearching }
|
||||||
|
func (m *unitSearching) SetValue(val bool) {
|
||||||
|
m.d.UnitSearching = val
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
m.d.UnitSearchString = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if m.d.UnitIndex < 0 || m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
|
||||||
|
m.d.UnitSearchString = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.d.UnitSearchString = m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Type
|
||||||
|
(*UnitModel)(m).updateDerivedUnitSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchTerm returns a String which is the search term user has typed when
|
||||||
|
// searching for units.
|
||||||
|
func (m *UnitModel) SearchTerm() String { return MakeString((*unitSearchTerm)(m)) }
|
||||||
|
|
||||||
|
type unitSearchTerm UnitModel
|
||||||
|
|
||||||
|
func (v *unitSearchTerm) Value() string {
|
||||||
|
// return current unit type string if not searching
|
||||||
|
if !v.d.UnitSearching {
|
||||||
|
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if v.d.UnitIndex < 0 || v.d.UnitIndex >= len(v.d.Song.Patch[v.d.InstrIndex].Units) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return v.d.Song.Patch[v.d.InstrIndex].Units[v.d.UnitIndex].Type
|
||||||
|
} else {
|
||||||
|
return v.d.UnitSearchString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (v *unitSearchTerm) SetValue(value string) bool {
|
||||||
|
v.d.UnitSearchString = value
|
||||||
|
v.d.UnitSearching = true
|
||||||
|
(*UnitModel)(v).updateDerivedUnitSearch()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *UnitModel) updateDerivedUnitSearch() {
|
||||||
|
// update search results based on current search string
|
||||||
|
v.derived.searchResults = v.derived.searchResults[:0]
|
||||||
|
for _, name := range sointu.UnitNames {
|
||||||
|
if strings.HasPrefix(name, v.SearchTerm().Value()) {
|
||||||
|
v.derived.searchResults = append(v.derived.searchResults, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchResult returns the unit search result at a given index.
|
||||||
|
func (l *UnitModel) SearchResult(index int) (name string, ok bool) {
|
||||||
|
if index < 0 || index >= len(l.derived.searchResults) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return l.derived.searchResults[index], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchResults returns a List of all the unit names matching the given search
|
||||||
|
// term.
|
||||||
|
func (m *UnitModel) SearchResults() List { return List{(*unitSearchResults)(m)} }
|
||||||
|
|
||||||
|
type unitSearchResults UnitModel
|
||||||
|
|
||||||
|
func (l *unitSearchResults) Selected() int { return l.d.UnitSearchIndex }
|
||||||
|
func (l *unitSearchResults) Selected2() int { return l.d.UnitSearchIndex }
|
||||||
|
func (l *unitSearchResults) SetSelected(value int) { l.d.UnitSearchIndex = value }
|
||||||
|
func (l *unitSearchResults) SetSelected2(value int) {}
|
||||||
|
func (l *unitSearchResults) Count() (count int) { return len(l.derived.searchResults) }
|
||||||
|
|
||||||
|
// Comment returns a String representing the comment string of the current unit.
|
||||||
|
func (m *UnitModel) Comment() String { return MakeString((*unitComment)(m)) }
|
||||||
|
|
||||||
|
type unitComment UnitModel
|
||||||
|
|
||||||
|
func (v *unitComment) Value() string {
|
||||||
|
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) ||
|
||||||
|
v.d.UnitIndex < 0 || v.d.UnitIndex >= len(v.d.Song.Patch[v.d.InstrIndex].Units) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return v.d.Song.Patch[v.d.InstrIndex].Units[v.d.UnitIndex].Comment
|
||||||
|
}
|
||||||
|
func (v *unitComment) SetValue(value string) bool {
|
||||||
|
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) ||
|
||||||
|
v.d.UnitIndex < 0 || v.d.UnitIndex >= len(v.d.Song.Patch[v.d.InstrIndex].Units) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer (*Model)(v).change("UnitComment", PatchChange, MinorChange)()
|
||||||
|
v.d.Song.Patch[v.d.InstrIndex].Units[v.d.UnitIndex].Comment = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disabled returns a Bool controlling whether the currently selected unit(s)
|
||||||
|
// are disabled.
|
||||||
|
func (m *UnitModel) Disabled() Bool { return MakeBool((*unitDisabled)(m)) }
|
||||||
|
|
||||||
|
type unitDisabled UnitModel
|
||||||
|
|
||||||
|
func (m *unitDisabled) Value() bool {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if m.d.UnitIndex < 0 || m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Disabled
|
||||||
|
}
|
||||||
|
func (m *unitDisabled) SetValue(val bool) {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l := ((*UnitModel)(m)).List()
|
||||||
|
r := l.listRange()
|
||||||
|
defer (*Model)(m).change("UnitDisabledSet", PatchChange, MajorChange)()
|
||||||
|
for i := r.Start; i < r.End; i++ {
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].Units[i].Disabled = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (m *unitDisabled) Enabled() bool {
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(m.d.Song.Patch[m.d.InstrIndex].Units) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item returns information about the unit at the given index.
|
||||||
|
func (v *UnitModel) Item(index int) UnitListItem {
|
||||||
|
i := v.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(v.d.Song.Patch) || index < 0 || index >= (*unitList)(v).Count() {
|
||||||
|
return UnitListItem{}
|
||||||
|
}
|
||||||
|
unit := v.d.Song.Patch[v.d.InstrIndex].Units[index]
|
||||||
|
signals := Rail{}
|
||||||
|
if i >= 0 && i < len(v.derived.patch) && index >= 0 && index < len(v.derived.patch[i].rails) {
|
||||||
|
signals = v.derived.patch[i].rails[index]
|
||||||
|
}
|
||||||
|
return UnitListItem{
|
||||||
|
Type: unit.Type,
|
||||||
|
Comment: unit.Comment,
|
||||||
|
Disabled: unit.Disabled,
|
||||||
|
Signals: signals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnitListItem struct {
|
||||||
|
Type, Comment string
|
||||||
|
Disabled bool
|
||||||
|
Signals Rail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the type of the currently selected unit.
|
||||||
|
func (m *UnitModel) Type() string {
|
||||||
|
if m.d.InstrIndex < 0 ||
|
||||||
|
m.d.InstrIndex >= len(m.d.Song.Patch) ||
|
||||||
|
m.d.UnitIndex < 0 ||
|
||||||
|
m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Type
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetType sets the type of the currently selected unit.
|
||||||
|
func (m *UnitModel) SetType(t string) {
|
||||||
|
if m.d.InstrIndex < 0 ||
|
||||||
|
m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if m.d.UnitIndex < 0 {
|
||||||
|
m.d.UnitIndex = 0
|
||||||
|
}
|
||||||
|
for len(m.d.Song.Patch[m.d.InstrIndex].Units) <= m.d.UnitIndex {
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].Units = append(m.d.Song.Patch[m.d.InstrIndex].Units, sointu.Unit{})
|
||||||
|
}
|
||||||
|
unit, ok := defaultUnits[t]
|
||||||
|
if !ok { // if the type is invalid, we just set it to empty unit
|
||||||
|
unit = sointu.Unit{Parameters: make(map[string]int)}
|
||||||
|
} else {
|
||||||
|
unit = unit.Copy()
|
||||||
|
}
|
||||||
|
oldUnit := m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex]
|
||||||
|
if oldUnit.Type == unit.Type {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer (*unitList)(m).Change("SetSelectedType", MajorChange)()
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex] = unit
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].ID = oldUnit.ID // keep the ID of the replaced unit
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns a List of all the units of the selected instrument, implementing
|
||||||
|
// ListData & MutableListData interfaces
|
||||||
|
func (m *UnitModel) List() List { return List{(*unitList)(m)} }
|
||||||
|
|
||||||
|
type unitList UnitModel
|
||||||
|
|
||||||
|
func (v *unitList) Selected() int { return v.d.UnitIndex }
|
||||||
|
func (v *unitList) Selected2() int { return v.d.UnitIndex2 }
|
||||||
|
func (v *unitList) SetSelected2(value int) { v.d.UnitIndex2 = value }
|
||||||
|
func (m *unitList) SetSelected(value int) {
|
||||||
|
m.d.UnitIndex = value
|
||||||
|
m.d.ParamIndex = 0
|
||||||
|
m.d.UnitSearching = false
|
||||||
|
m.d.UnitSearchString = ""
|
||||||
|
}
|
||||||
|
func (v *unitList) Count() int {
|
||||||
|
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(v.d.Song.Patch[v.d.InstrIndex].Units)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *unitList) Move(r Range, delta int) (ok bool) {
|
||||||
|
m := (*Model)(v)
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
units := m.d.Song.Patch[m.d.InstrIndex].Units
|
||||||
|
for i, j := range r.Swaps(delta) {
|
||||||
|
units[i], units[j] = units[j], units[i]
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *unitList) Delete(r Range) (ok bool) {
|
||||||
|
m := (*Model)(v)
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
u := m.d.Song.Patch[m.d.InstrIndex].Units
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].Units = append(u[:r.Start], u[r.End:]...)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *unitList) Change(n string, severity ChangeSeverity) func() {
|
||||||
|
return (*Model)(v).change("UnitListView."+n, PatchChange, severity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *unitList) Cancel() {
|
||||||
|
(*Model)(v).changeCancel = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *unitList) Marshal(r Range) ([]byte, error) {
|
||||||
|
m := (*Model)(v)
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return nil, errors.New("UnitListView.marshal: no instruments")
|
||||||
|
}
|
||||||
|
units := m.d.Song.Patch[m.d.InstrIndex].Units[r.Start:r.End]
|
||||||
|
ret, err := yaml.Marshal(struct{ Units []sointu.Unit }{units})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("UnitListView.marshal: %v", err)
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *unitList) Unmarshal(data []byte) (r Range, err error) {
|
||||||
|
m := (*Model)(v)
|
||||||
|
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||||
|
return Range{}, errors.New("UnitListView.unmarshal: no instruments")
|
||||||
|
}
|
||||||
|
var pastedUnits struct{ Units []sointu.Unit }
|
||||||
|
if err := yaml.Unmarshal(data, &pastedUnits); err != nil {
|
||||||
|
return Range{}, fmt.Errorf("UnitListView.unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if len(pastedUnits.Units) == 0 {
|
||||||
|
return Range{}, errors.New("UnitListView.unmarshal: no units")
|
||||||
|
}
|
||||||
|
m.assignUnitIDs(pastedUnits.Units)
|
||||||
|
sel := v.Selected()
|
||||||
|
var ok bool
|
||||||
|
m.d.Song.Patch[m.d.InstrIndex].Units, ok = Insert(m.d.Song.Patch[m.d.InstrIndex].Units, sel, pastedUnits.Units...)
|
||||||
|
if !ok {
|
||||||
|
return Range{}, errors.New("UnitListView.unmarshal: insert failed")
|
||||||
|
}
|
||||||
|
return Range{sel, sel + len(pastedUnits.Units)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UnitModel) RailError() RailError { return s.derived.railError }
|
||||||
|
|
||||||
|
func (s *UnitModel) RailWidth() int {
|
||||||
|
i := s.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(s.derived.patch) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return s.derived.patch[i].railWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RailError) Error() string { return e.Err.Error() }
|
||||||
|
|
||||||
|
func (s *Rail) StackAfter() int { return s.PassThrough + s.StackUse.NumOutputs }
|
||||||
191
tracker/voices.go
Normal file
191
tracker/voices.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
|
"github.com/vsariola/sointu/vm"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VoiceSlice works similar to the Slice function, but takes a slice of
|
||||||
|
// NumVoicer:s and treats it as a "virtual slice", with element repeated by the
|
||||||
|
// number of voices it has. NumVoicer interface is implemented at least by
|
||||||
|
// sointu.Tracks and sointu.Instruments. For example, if parameter "slice" has
|
||||||
|
// three elements, returning GetNumVoices 2, 1, and 3, the VoiceSlice thinks of
|
||||||
|
// this as a virtual slice of 6 elements [0,0,1,2,2,2]. Then, the "ranges"
|
||||||
|
// parameter are slicing ranges to this virtual slice. Continuing with the
|
||||||
|
// example, if "ranges" was [2,5), the virtual slice would be [1,2,2], and the
|
||||||
|
// function would return a slice with two elements: first with NumVoices 1 and
|
||||||
|
// second with NumVoices 2. If multiple ranges are given, multiple virtual
|
||||||
|
// slices are concatenated. However, when doing so, splitting an element is not
|
||||||
|
// allowed. In the previous example, if the ranges were [1,3) and [0,1), the
|
||||||
|
// resulting concatenated virtual slice would be [0,1,0], and here the 0 element
|
||||||
|
// would be split. This is to avoid accidentally making shallow copies of
|
||||||
|
// reference types.
|
||||||
|
func VoiceSlice[T any, S ~[]T, P sointu.NumVoicerPointer[T]](slice S, ranges ...Range) (ret S, ok bool) {
|
||||||
|
ret = make(S, 0, len(slice))
|
||||||
|
last := -1
|
||||||
|
used := make([]bool, len(slice))
|
||||||
|
outer:
|
||||||
|
for _, r := range ranges {
|
||||||
|
left := 0
|
||||||
|
for i, elem := range slice {
|
||||||
|
right := left + (P)(&slice[i]).GetNumVoices()
|
||||||
|
if left >= r.End {
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
if right <= r.Start {
|
||||||
|
left = right
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
overlap := min(right, r.End) - max(left, r.Start)
|
||||||
|
if last == i {
|
||||||
|
(P)(&ret[len(ret)-1]).SetNumVoices(
|
||||||
|
(P)(&ret[len(ret)-1]).GetNumVoices() + overlap)
|
||||||
|
} else {
|
||||||
|
if last == math.MaxInt || used[i] {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
ret = append(ret, elem)
|
||||||
|
(P)(&ret[len(ret)-1]).SetNumVoices(overlap)
|
||||||
|
used[i] = true
|
||||||
|
}
|
||||||
|
last = i
|
||||||
|
left = right
|
||||||
|
}
|
||||||
|
if left >= r.End {
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
last = math.MaxInt // the list is closed, adding more elements causes it to fail
|
||||||
|
}
|
||||||
|
return ret, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// VoiceInsert tries adding the elements "added" to the slice "orig" at the
|
||||||
|
// voice index "index". Notice that index is the index into a virtual slice
|
||||||
|
// where each element is repeated by the number of voices it has. If the index
|
||||||
|
// is between elements, the new elements are added in between the old elements.
|
||||||
|
// If the addition would cause splitting of an element, we rather increase the
|
||||||
|
// number of voices the element has, but do not split it.
|
||||||
|
func VoiceInsert[T any, S ~[]T, P sointu.NumVoicerPointer[T]](orig S, index, length int, added ...T) (ret S, retRange Range, ok bool) {
|
||||||
|
ret = make(S, 0, len(orig)+length)
|
||||||
|
left := 0
|
||||||
|
for i, elem := range orig {
|
||||||
|
right := left + (P)(&orig[i]).GetNumVoices()
|
||||||
|
if left == index { // we are between elements and it's safe to add there
|
||||||
|
if sointu.TotalVoices[T, S, P](added) < length {
|
||||||
|
return nil, Range{}, false // we are missing some elements
|
||||||
|
}
|
||||||
|
retRange = Range{len(ret), len(ret) + len(added)}
|
||||||
|
ret = append(ret, added...)
|
||||||
|
} else if left < index && index < right { // we are inside an element and would split it; just increase its voices instead of splitting
|
||||||
|
(P)(&elem).SetNumVoices((P)(&orig[i]).GetNumVoices() + sointu.TotalVoices[T, S, P](added))
|
||||||
|
retRange = Range{len(ret), len(ret)}
|
||||||
|
}
|
||||||
|
ret = append(ret, elem)
|
||||||
|
left = right
|
||||||
|
}
|
||||||
|
if left == index { // we are at the end and it's safe to add there, even if we are missing some elements
|
||||||
|
retRange = Range{len(ret), len(ret) + len(added)}
|
||||||
|
ret = append(ret, added...)
|
||||||
|
}
|
||||||
|
return ret, retRange, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func VoiceRange[T any, S ~[]T, P sointu.NumVoicerPointer[T]](slice S, indexRange Range) (voiceRange Range) {
|
||||||
|
indexRange.Start = max(0, indexRange.Start)
|
||||||
|
indexRange.End = min(len(slice), indexRange.End)
|
||||||
|
for _, e := range slice[:indexRange.Start] {
|
||||||
|
voiceRange.Start += (P)(&e).GetNumVoices()
|
||||||
|
}
|
||||||
|
voiceRange.End = voiceRange.Start
|
||||||
|
for i := indexRange.Start; i < indexRange.End; i++ {
|
||||||
|
voiceRange.End += (P)(&slice[i]).GetNumVoices()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
|
||||||
|
func (m *Model) sliceInstrumentsTracks(instruments, tracks bool, ranges ...Range) (ok bool) {
|
||||||
|
defer m.change("sliceInstrumentsTracks", PatchChange, MajorChange)()
|
||||||
|
if instruments {
|
||||||
|
m.d.Song.Patch, ok = VoiceSlice(m.d.Song.Patch, ranges...)
|
||||||
|
if !ok {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tracks {
|
||||||
|
m.d.Song.Score.Tracks, ok = VoiceSlice(m.d.Song.Score.Tracks, ranges...)
|
||||||
|
if !ok {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
fail:
|
||||||
|
(*Model)(m).Alerts().AddNamed("slicesInstrumentsTracks", "Modify prevented by Instrument-Track linking", Warning)
|
||||||
|
m.changeCancel = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) marshalVoices(r Range) (data []byte, err error) {
|
||||||
|
patch, ok := VoiceSlice(m.d.Song.Patch, r)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("marshalVoiceRange: slicing patch failed")
|
||||||
|
}
|
||||||
|
tracks, ok := VoiceSlice(m.d.Song.Score.Tracks, r)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("marshalVoiceRange: slicing tracks failed")
|
||||||
|
}
|
||||||
|
return yaml.Marshal(struct {
|
||||||
|
Patch sointu.Patch
|
||||||
|
Tracks []sointu.Track
|
||||||
|
}{patch, tracks})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) unmarshalVoices(voiceIndex int, data []byte, instruments, tracks bool) (instrRange, trackRange Range, ok bool) {
|
||||||
|
var d struct {
|
||||||
|
Patch sointu.Patch
|
||||||
|
Tracks []sointu.Track
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(data, &d); err != nil {
|
||||||
|
return Range{}, Range{}, false
|
||||||
|
}
|
||||||
|
return m.addVoices(voiceIndex, d.Patch, d.Tracks, instruments, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) addVoices(voiceIndex int, p sointu.Patch, t []sointu.Track, instruments, tracks bool) (instrRange Range, trackRange Range, ok bool) {
|
||||||
|
defer m.change("addVoices", PatchChange, MajorChange)()
|
||||||
|
addedLength := max(p.NumVoices(), sointu.TotalVoices(t))
|
||||||
|
if instruments {
|
||||||
|
m.assignUnitIDsForPatch(p)
|
||||||
|
m.d.Song.Patch, instrRange, ok = VoiceInsert(m.d.Song.Patch, voiceIndex, addedLength, p...)
|
||||||
|
if !ok {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tracks {
|
||||||
|
m.d.Song.Score.Tracks, trackRange, ok = VoiceInsert(m.d.Song.Score.Tracks, voiceIndex, addedLength, t...)
|
||||||
|
if !ok {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instrRange, trackRange, true
|
||||||
|
fail:
|
||||||
|
(*Model)(m).Alerts().AddNamed("addVoices", "Adding voices prevented by Instrument-Track linking", Warning)
|
||||||
|
m.changeCancel = true
|
||||||
|
return Range{}, Range{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) remainingVoices(instruments, tracks bool) (ret int) {
|
||||||
|
ret = math.MaxInt
|
||||||
|
if instruments {
|
||||||
|
ret = min(ret, vm.MAX_VOICES-m.d.Song.Patch.NumVoices())
|
||||||
|
}
|
||||||
|
if tracks {
|
||||||
|
ret = min(ret, vm.MAX_VOICES-m.d.Song.Score.NumVoices())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user