mirror of
https://github.com/vsariola/sointu.git
synced 2026-04-03 20:53:04 -04:00
refactor(tracker): group Model methods, with each group in one source file
This commit is contained in:
parent
b93304adab
commit
86ca3fb300
@ -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
|
||||
}
|
||||
|
||||
AlertPriority int
|
||||
AlertYieldFunc func(index int, alert Alert) bool
|
||||
Alerts Model
|
||||
AlertPriority int
|
||||
)
|
||||
|
||||
const (
|
||||
@ -29,12 +27,12 @@ const (
|
||||
Error
|
||||
)
|
||||
|
||||
// Model methods
|
||||
|
||||
// Alerts returns the Alerts model from the main Model, used to manage alerts.
|
||||
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) {
|
||||
for i, a := range m.alerts {
|
||||
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) {
|
||||
for i := len(m.alerts) - 1; i >= 0; i-- {
|
||||
if m.alerts[i].Duration >= d {
|
||||
@ -66,6 +66,7 @@ func (m *Alerts) Update(d time.Duration) (animating bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// Add a new alert with the given message and priority.
|
||||
func (m *Alerts) Add(message string, priority AlertPriority) {
|
||||
m.AddAlert(Alert{
|
||||
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) {
|
||||
m.AddAlert(Alert{
|
||||
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) {
|
||||
for i := range m.alerts {
|
||||
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) {
|
||||
for i := range m.alerts {
|
||||
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 {
|
||||
SpecSettings SpecAnSettings
|
||||
SpecSettings specAnSettings
|
||||
HasSettings bool
|
||||
Data any
|
||||
}
|
||||
|
||||
@ -36,7 +36,6 @@ type (
|
||||
patch []derivedInstrument
|
||||
tracks []derivedTrack
|
||||
railError RailError
|
||||
presetSearch derivedPresetSearch
|
||||
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
|
||||
|
||||
func (m *Model) updateDeriveData(changeType ChangeType) {
|
||||
|
||||
@ -7,28 +7,95 @@ import (
|
||||
"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 (
|
||||
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
|
||||
loudnessDetector loudnessDetector
|
||||
peakDetector peakDetector
|
||||
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 {
|
||||
weighting weighting
|
||||
states [2][3]biquadState
|
||||
@ -62,52 +129,14 @@ type (
|
||||
history [11]float32
|
||||
tmp, tmp2 []float32
|
||||
}
|
||||
|
||||
chunker struct {
|
||||
buffer sointu.AudioBuffer
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
LoudnessMomentary LoudnessType = iota
|
||||
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{
|
||||
func runDetector(b *Broker) {
|
||||
s := &detector{
|
||||
broker: b,
|
||||
loudnessDetector: makeLoudnessDetector(KWeighting),
|
||||
peakDetector: makePeakDetector(true),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Detector) Run() {
|
||||
for {
|
||||
select {
|
||||
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 {
|
||||
s.loudnessDetector.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)) {
|
||||
c.buffer = append(c.buffer, input...)
|
||||
b := c.buffer
|
||||
|
||||
@ -1,4 +1,23 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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 {
|
||||
ret := &InstrumentEditor{
|
||||
dragList: NewDragList(m.Units(), layout.Vertical),
|
||||
dragList: NewDragList(m.Unit().List(), layout.Vertical),
|
||||
addUnitBtn: new(Clickable),
|
||||
searchEditor: NewEditor(true, true, text.Start),
|
||||
DeleteUnitBtn: new(Clickable),
|
||||
@ -62,9 +62,9 @@ func NewInstrumentEditor(m *tracker.Model) *InstrumentEditor {
|
||||
CopyUnitBtn: new(Clickable),
|
||||
SelectTypeBtn: new(Clickable),
|
||||
commentEditor: NewEditor(true, true, text.Start),
|
||||
paramTable: NewScrollTable(m.Params().Table(), m.ParamVertList().List(), m.Units()),
|
||||
searchList: NewDragList(m.SearchResults(), layout.Vertical),
|
||||
searching: m.UnitSearching(),
|
||||
paramTable: NewScrollTable(m.Params().Table(), m.Params().Columns(), m.Unit().List()),
|
||||
searchList: NewDragList(m.Unit().SearchResults(), layout.Vertical),
|
||||
searching: m.Unit().Searching(),
|
||||
}
|
||||
ret.caser = cases.Title(language.English)
|
||||
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 {
|
||||
gtx.Constraints.Max.Y = gtx.Dp(20)
|
||||
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||
u := t.Unit(i)
|
||||
u := t.Unit().Item(i)
|
||||
editorStyle := t.Theme.InstrumentEditor.UnitList.Name
|
||||
signalError := t.RailError()
|
||||
signalError := t.Unit().RailError()
|
||||
switch {
|
||||
case u.Disabled:
|
||||
editorStyle = t.Theme.InstrumentEditor.UnitList.NameDisabled
|
||||
@ -107,7 +107,7 @@ func (ul *InstrumentEditor) layoutList(gtx C) D {
|
||||
unitName := func(gtx C) D {
|
||||
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()
|
||||
return ul.searchEditor.Layout(gtx, t.Model.UnitSearch(), t.Theme, &editorStyle, "---")
|
||||
return ul.searchEditor.Layout(gtx, t.Model.Unit().SearchTerm(), t.Theme, &editorStyle, "---")
|
||||
} else {
|
||||
text := u.Type
|
||||
if text == "" {
|
||||
@ -169,40 +169,40 @@ func (ul *InstrumentEditor) update(gtx C) {
|
||||
case key.NameRightArrow:
|
||||
t.PatchPanel.instrEditor.paramTable.RowTitleList.Focus()
|
||||
case key.NameDeleteBackward:
|
||||
t.SetSelectedUnitType("")
|
||||
t.UnitSearching().SetValue(true)
|
||||
t.Unit().SetType("")
|
||||
t.Unit().Searching().SetValue(true)
|
||||
ul.searchEditor.Focus()
|
||||
case key.NameEnter, key.NameReturn:
|
||||
t.Model.AddUnit(e.Modifiers.Contain(key.ModCtrl)).Do()
|
||||
t.UnitSearching().SetValue(true)
|
||||
t.Model.Unit().Add(e.Modifiers.Contain(key.ModCtrl)).Do()
|
||||
t.Unit().Searching().SetValue(true)
|
||||
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) {
|
||||
if ev == EditorEventSubmit {
|
||||
if str.Value() != "" {
|
||||
for _, n := range sointu.UnitNames {
|
||||
if strings.HasPrefix(n, str.Value()) {
|
||||
t.SetSelectedUnitType(n)
|
||||
t.Unit().SetType(n)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.SetSelectedUnitType("")
|
||||
t.Unit().SetType("")
|
||||
}
|
||||
}
|
||||
ul.dragList.Focus()
|
||||
t.UnitSearching().SetValue(false)
|
||||
t.Unit().Searching().SetValue(false)
|
||||
}
|
||||
for ul.addUnitBtn.Clicked(gtx) {
|
||||
t.AddUnit(false).Do()
|
||||
t.UnitSearching().SetValue(true)
|
||||
t.Unit().Add(false).Do()
|
||||
t.Unit().Searching().SetValue(true)
|
||||
ul.searchEditor.Focus()
|
||||
}
|
||||
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))})
|
||||
t.Alerts().Add("Unit(s) copied to clipboard", tracker.Info)
|
||||
}
|
||||
@ -211,9 +211,9 @@ func (ul *InstrumentEditor) update(gtx C) {
|
||||
ul.ChooseUnitType(t)
|
||||
}
|
||||
for ul.ClearUnitBtn.Clicked(gtx) {
|
||||
t.ClearUnit().Do()
|
||||
t.UnitSearch().SetValue("")
|
||||
t.UnitSearching().SetValue(true)
|
||||
t.Unit().Clear().Do()
|
||||
t.Unit().SearchTerm().SetValue("")
|
||||
t.Unit().Searching().SetValue(true)
|
||||
ul.searchList.Focus()
|
||||
}
|
||||
for {
|
||||
@ -228,7 +228,7 @@ func (ul *InstrumentEditor) update(gtx C) {
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
switch e.Name {
|
||||
case key.NameEscape:
|
||||
t.UnitSearching().SetValue(false)
|
||||
t.Unit().Searching().SetValue(false)
|
||||
ul.paramTable.RowTitleList.Focus()
|
||||
case key.NameEnter, key.NameReturn:
|
||||
ul.ChooseUnitType(t)
|
||||
@ -288,8 +288,8 @@ func (pe *InstrumentEditor) layoutTable(gtx C) D {
|
||||
}
|
||||
|
||||
func (pe *InstrumentEditor) ChooseUnitType(t *Tracker) {
|
||||
if ut, ok := t.SearchResult(pe.searchList.TrackerList.Selected()); ok {
|
||||
t.SetSelectedUnitType(ut)
|
||||
if ut, ok := t.Unit().SearchResult(pe.searchList.TrackerList.Selected()); ok {
|
||||
t.Unit().SetType(ut)
|
||||
pe.paramTable.RowTitleList.Focus()
|
||||
}
|
||||
}
|
||||
@ -305,9 +305,9 @@ func (pe *InstrumentEditor) layoutRack(gtx C) D {
|
||||
cellWidth := gtx.Dp(t.Theme.UnitEditor.Width)
|
||||
cellHeight := gtx.Dp(t.Theme.UnitEditor.Height)
|
||||
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
|
||||
signalError := t.RailError()
|
||||
signalError := t.Unit().RailError()
|
||||
columnTitleHeight := gtx.Dp(0)
|
||||
for i := range pe.Parameters {
|
||||
for len(pe.Parameters[i]) < width {
|
||||
@ -321,7 +321,7 @@ func (pe *InstrumentEditor) layoutRack(gtx C) D {
|
||||
if y < 0 || y >= len(pe.Parameters) {
|
||||
return D{}
|
||||
}
|
||||
item := t.Unit(y)
|
||||
item := t.Unit().Item(y)
|
||||
sr := Rail(t.Theme, item.Signals)
|
||||
label := Label(t.Theme, &t.Theme.UnitEditor.UnitList.Name, item.Type)
|
||||
switch {
|
||||
@ -360,20 +360,20 @@ func (pe *InstrumentEditor) layoutRack(gtx C) D {
|
||||
}
|
||||
|
||||
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)
|
||||
if x == t.Model.Params().RowWidth(y) {
|
||||
if y == cursor.Y {
|
||||
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)
|
||||
}
|
||||
gtx.Constraints.Max.X = 1e6
|
||||
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 {
|
||||
comment := t.Unit(y).Comment
|
||||
comment := t.Unit().Item(y).Comment
|
||||
if comment != "" {
|
||||
style := t.Theme.InstrumentEditor.UnitComment.AsLabelStyle()
|
||||
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)
|
||||
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()
|
||||
for wire := range t.Wires {
|
||||
for wire := range t.Params().Wires {
|
||||
clr := t.Theme.UnitEditor.WireColor
|
||||
if wire.Highlight {
|
||||
clr = t.Theme.UnitEditor.WireHighlight
|
||||
@ -516,9 +516,9 @@ func mulVec(a, b f32.Point) f32.Point {
|
||||
|
||||
func (pe *InstrumentEditor) layoutFooter(gtx C) D {
|
||||
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)
|
||||
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")
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(deleteUnitBtn.Layout),
|
||||
@ -531,7 +531,7 @@ func (pe *InstrumentEditor) layoutFooter(gtx C) D {
|
||||
func (pe *InstrumentEditor) layoutUnitTypeChooser(gtx C) D {
|
||||
t := TrackerFromContext(gtx)
|
||||
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)
|
||||
if i == pe.searchList.TrackerList.Selected() {
|
||||
return pe.SelectTypeBtn.Layout(gtx, w.Layout)
|
||||
|
||||
@ -36,8 +36,8 @@ func NewInstrumentPresets(m *tracker.Model) *InstrumentPresets {
|
||||
builtinPresetsBtn: new(Clickable),
|
||||
saveUserPreset: new(Clickable),
|
||||
deleteUserPreset: new(Clickable),
|
||||
dirList: NewDragList(m.PresetDirList().List(), layout.Vertical),
|
||||
resultList: NewDragList(m.PresetResultList().List(), layout.Vertical),
|
||||
dirList: NewDragList(m.Preset().DirList(), layout.Vertical),
|
||||
resultList: NewDragList(m.Preset().SearchResultList(), layout.Vertical),
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,13 +88,13 @@ func (ip *InstrumentPresets) layout(gtx C) D {
|
||||
ip.update(gtx)
|
||||
// get tracker from values
|
||||
tr := TrackerFromContext(gtx)
|
||||
gmDlsBtn := ToggleBtn(tr.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")
|
||||
builtinPresetsFilterBtn := ToggleBtn(tr.BuiltinPresetsFilter(), tr.Theme, ip.builtinPresetsBtn, "Builtin", "Show only builtin presets")
|
||||
saveUserPresetBtn := ActionIconBtn(tr.SaveAsUserPreset(), tr.Theme, ip.saveUserPreset, icons.ContentSave, "Save instrument as user preset")
|
||||
deleteUserPresetBtn := ActionIconBtn(tr.TryDeleteUserPreset(), tr.Theme, ip.deleteUserPreset, icons.ActionDelete, "Delete user preset")
|
||||
gmDlsBtn := ToggleBtn(tr.Preset().NoGmDls(), tr.Theme, ip.gmDlsBtn, "No gm.dls", "Exclude presets using gm.dls")
|
||||
userPresetsFilterBtn := ToggleBtn(tr.Preset().UserFilter(), tr.Theme, ip.userPresetsBtn, "User", "Show only user presets")
|
||||
builtinPresetsFilterBtn := ToggleBtn(tr.Preset().BuiltinFilter(), tr.Theme, ip.builtinPresetsBtn, "Builtin", "Show only builtin presets")
|
||||
saveUserPresetBtn := ActionIconBtn(tr.Preset().Save(), tr.Theme, ip.saveUserPreset, icons.ContentSave, "Save instrument as user preset")
|
||||
deleteUserPresetBtn := ActionIconBtn(tr.Preset().Delete(), tr.Theme, ip.deleteUserPreset, icons.ActionDelete, "Delete user preset")
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
ln := Label(tr.Theme, &tr.Theme.InstrumentEditor.Presets.Results.User, n)
|
||||
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)
|
||||
}
|
||||
floatButtons := func(gtx C) D {
|
||||
if tr.Model.DeleteUserPreset().Enabled() {
|
||||
if tr.Model.Preset().Delete().Enabled() {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(deleteUserPresetBtn.Layout),
|
||||
layout.Rigid(saveUserPresetBtn.Layout),
|
||||
@ -189,10 +189,10 @@ func (ip *InstrumentPresets) layoutSearch(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 {
|
||||
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)
|
||||
}
|
||||
w := func(gtx C) D {
|
||||
|
||||
@ -58,20 +58,20 @@ func (ip *InstrumentProperties) layout(gtx C) D {
|
||||
// get tracker from values
|
||||
tr := TrackerFromContext(gtx)
|
||||
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,
|
||||
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)
|
||||
}),
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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.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.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.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 {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
@ -86,21 +86,21 @@ func (ip *InstrumentProperties) layout(gtx C) D {
|
||||
switch index {
|
||||
case 0:
|
||||
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:
|
||||
return layoutInstrumentPropertyLine(gtx, "Voices", voiceLine)
|
||||
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)
|
||||
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)
|
||||
case 8:
|
||||
return layoutInstrumentPropertyLine(gtx, "Thread", threadbtnline)
|
||||
case 10:
|
||||
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
|
||||
px := max(gtx.Dp(unit.Dp(1)), 1)
|
||||
|
||||
@ -102,93 +102,93 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
||||
switch action {
|
||||
// Actions
|
||||
case "AddTrack":
|
||||
t.AddTrack().Do()
|
||||
t.Track().Add().Do()
|
||||
case "DeleteTrack":
|
||||
t.DeleteTrack().Do()
|
||||
t.Track().Delete().Do()
|
||||
case "AddInstrument":
|
||||
t.AddInstrument().Do()
|
||||
t.Instrument().Add().Do()
|
||||
case "DeleteInstrument":
|
||||
t.DeleteInstrument().Do()
|
||||
t.Instrument().Delete().Do()
|
||||
case "AddUnitAfter":
|
||||
t.AddUnit(false).Do()
|
||||
t.Unit().Add(false).Do()
|
||||
case "AddUnitBefore":
|
||||
t.AddUnit(true).Do()
|
||||
t.Unit().Add(true).Do()
|
||||
case "DeleteUnit":
|
||||
t.DeleteUnit().Do()
|
||||
t.Unit().Delete().Do()
|
||||
case "ClearUnit":
|
||||
t.ClearUnit().Do()
|
||||
t.Unit().Clear().Do()
|
||||
case "Undo":
|
||||
t.Undo().Do()
|
||||
t.History().Undo().Do()
|
||||
case "Redo":
|
||||
t.Redo().Do()
|
||||
t.History().Redo().Do()
|
||||
case "AddSemitone":
|
||||
t.AddSemitone().Do()
|
||||
t.Note().AddSemitone().Do()
|
||||
case "SubtractSemitone":
|
||||
t.SubtractSemitone().Do()
|
||||
t.Note().SubtractSemitone().Do()
|
||||
case "AddOctave":
|
||||
t.AddOctave().Do()
|
||||
t.Note().AddOctave().Do()
|
||||
case "SubtractOctave":
|
||||
t.SubtractOctave().Do()
|
||||
t.Note().SubtractOctave().Do()
|
||||
case "EditNoteOff":
|
||||
t.EditNoteOff().Do()
|
||||
t.Note().NoteOff().Do()
|
||||
case "RemoveUnused":
|
||||
t.RemoveUnused().Do()
|
||||
t.Order().RemoveUnusedPatterns().Do()
|
||||
case "PlayCurrentPosFollow":
|
||||
t.Follow().SetValue(true)
|
||||
t.PlayCurrentPos().Do()
|
||||
t.Play().IsFollowing().SetValue(true)
|
||||
t.Play().FromCurrentPos().Do()
|
||||
case "PlayCurrentPosUnfollow":
|
||||
t.Follow().SetValue(false)
|
||||
t.PlayCurrentPos().Do()
|
||||
t.Play().IsFollowing().SetValue(false)
|
||||
t.Play().FromCurrentPos().Do()
|
||||
case "PlaySongStartFollow":
|
||||
t.Follow().SetValue(true)
|
||||
t.PlaySongStart().Do()
|
||||
t.Play().IsFollowing().SetValue(true)
|
||||
t.Play().FromBeginning().Do()
|
||||
case "PlaySongStartUnfollow":
|
||||
t.Follow().SetValue(false)
|
||||
t.PlaySongStart().Do()
|
||||
t.Play().IsFollowing().SetValue(false)
|
||||
t.Play().FromBeginning().Do()
|
||||
case "PlaySelectedFollow":
|
||||
t.Follow().SetValue(true)
|
||||
t.PlaySelected().Do()
|
||||
t.Play().IsFollowing().SetValue(true)
|
||||
t.Play().FromSelected().Do()
|
||||
case "PlaySelectedUnfollow":
|
||||
t.Follow().SetValue(false)
|
||||
t.PlaySelected().Do()
|
||||
t.Play().IsFollowing().SetValue(false)
|
||||
t.Play().FromSelected().Do()
|
||||
case "PlayLoopFollow":
|
||||
t.Follow().SetValue(true)
|
||||
t.PlayFromLoopStart().Do()
|
||||
t.Play().IsFollowing().SetValue(true)
|
||||
t.Play().FromLoopBeginning().Do()
|
||||
case "PlayLoopUnfollow":
|
||||
t.Follow().SetValue(false)
|
||||
t.PlayFromLoopStart().Do()
|
||||
t.Play().IsFollowing().SetValue(false)
|
||||
t.Play().FromLoopBeginning().Do()
|
||||
case "StopPlaying":
|
||||
t.StopPlaying().Do()
|
||||
t.Play().Stop().Do()
|
||||
case "AddOrderRowBefore":
|
||||
t.AddOrderRow(true).Do()
|
||||
t.Order().AddRow(true).Do()
|
||||
case "AddOrderRowAfter":
|
||||
t.AddOrderRow(false).Do()
|
||||
t.Order().AddRow(false).Do()
|
||||
case "DeleteOrderRowBackwards":
|
||||
t.DeleteOrderRow(true).Do()
|
||||
t.Order().DeleteRow(true).Do()
|
||||
case "DeleteOrderRowForwards":
|
||||
t.DeleteOrderRow(false).Do()
|
||||
t.Order().DeleteRow(false).Do()
|
||||
case "NewSong":
|
||||
t.NewSong().Do()
|
||||
t.Song().New().Do()
|
||||
case "OpenSong":
|
||||
t.OpenSong().Do()
|
||||
t.Song().Open().Do()
|
||||
case "Quit":
|
||||
if canQuit {
|
||||
t.RequestQuit().Do()
|
||||
}
|
||||
case "SaveSong":
|
||||
t.SaveSong().Do()
|
||||
t.Song().Save().Do()
|
||||
case "SaveSongAs":
|
||||
t.SaveSongAs().Do()
|
||||
t.Song().SaveAs().Do()
|
||||
case "ExportWav":
|
||||
t.Export().Do()
|
||||
t.Song().Export().Do()
|
||||
case "ExportFloat":
|
||||
t.ExportFloat().Do()
|
||||
t.Song().ExportFloat().Do()
|
||||
case "ExportInt16":
|
||||
t.ExportInt16().Do()
|
||||
t.Song().ExportInt16().Do()
|
||||
case "SplitTrack":
|
||||
t.SplitTrack().Do()
|
||||
t.Track().Split().Do()
|
||||
case "SplitInstrument":
|
||||
t.SplitInstrument().Do()
|
||||
t.Instrument().Split().Do()
|
||||
case "ShowManual":
|
||||
t.ShowManual().Do()
|
||||
case "AskHelp":
|
||||
@ -199,72 +199,72 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
||||
t.ShowLicense().Do()
|
||||
// Booleans
|
||||
case "PanicToggle":
|
||||
t.Panic().Toggle()
|
||||
t.Play().Panicked().Toggle()
|
||||
case "RecordingToggle":
|
||||
t.IsRecording().Toggle()
|
||||
t.Play().IsRecording().Toggle()
|
||||
case "PlayingToggleFollow":
|
||||
t.Follow().SetValue(true)
|
||||
t.Playing().Toggle()
|
||||
t.Play().IsFollowing().SetValue(true)
|
||||
t.Play().Started().Toggle()
|
||||
case "PlayingToggleUnfollow":
|
||||
t.Follow().SetValue(false)
|
||||
t.Playing().Toggle()
|
||||
t.Play().IsFollowing().SetValue(false)
|
||||
t.Play().Started().Toggle()
|
||||
case "InstrEnlargedToggle":
|
||||
t.InstrEnlarged().Toggle()
|
||||
t.Play().TrackerHidden().Toggle()
|
||||
case "LinkInstrTrackToggle":
|
||||
t.LinkInstrTrack().Toggle()
|
||||
t.Track().LinkInstrument().Toggle()
|
||||
case "FollowToggle":
|
||||
t.Follow().Toggle()
|
||||
t.Play().IsFollowing().Toggle()
|
||||
case "UnitDisabledToggle":
|
||||
t.UnitDisabled().Toggle()
|
||||
t.Unit().Disabled().Toggle()
|
||||
case "LoopToggle":
|
||||
t.LoopToggle().Toggle()
|
||||
t.Play().IsLooping().Toggle()
|
||||
case "UniquePatternsToggle":
|
||||
t.UniquePatterns().Toggle()
|
||||
t.Note().UniquePatterns().Toggle()
|
||||
case "MuteToggle":
|
||||
t.Mute().Toggle()
|
||||
t.Instrument().Mute().Toggle()
|
||||
case "SoloToggle":
|
||||
t.Solo().Toggle()
|
||||
t.Instrument().Solo().Toggle()
|
||||
// Integers
|
||||
case "InstrumentVoicesAdd":
|
||||
t.Model.InstrumentVoices().Add(1)
|
||||
t.Instrument().Voices().Add(1)
|
||||
case "InstrumentVoicesSubtract":
|
||||
t.Model.InstrumentVoices().Add(-1)
|
||||
t.Instrument().Voices().Add(-1)
|
||||
case "TrackVoicesAdd":
|
||||
t.TrackVoices().Add(1)
|
||||
t.Track().Voices().Add(1)
|
||||
case "TrackVoicesSubtract":
|
||||
t.TrackVoices().Add(-1)
|
||||
t.Track().Voices().Add(-1)
|
||||
case "SongLengthAdd":
|
||||
t.SongLength().Add(1)
|
||||
t.Song().Length().Add(1)
|
||||
case "SongLengthSubtract":
|
||||
t.SongLength().Add(-1)
|
||||
t.Song().Length().Add(-1)
|
||||
case "BPMAdd":
|
||||
t.BPM().Add(1)
|
||||
t.Song().BPM().Add(1)
|
||||
case "BPMSubtract":
|
||||
t.BPM().Add(-1)
|
||||
t.Song().BPM().Add(-1)
|
||||
case "RowsPerPatternAdd":
|
||||
t.RowsPerPattern().Add(1)
|
||||
t.Song().RowsPerPattern().Add(1)
|
||||
case "RowsPerPatternSubtract":
|
||||
t.RowsPerPattern().Add(-1)
|
||||
t.Song().RowsPerPattern().Add(-1)
|
||||
case "RowsPerBeatAdd":
|
||||
t.RowsPerBeat().Add(1)
|
||||
t.Song().RowsPerBeat().Add(1)
|
||||
case "RowsPerBeatSubtract":
|
||||
t.RowsPerBeat().Add(-1)
|
||||
t.Song().RowsPerBeat().Add(-1)
|
||||
case "StepAdd":
|
||||
t.Step().Add(1)
|
||||
t.Note().Step().Add(1)
|
||||
case "StepSubtract":
|
||||
t.Step().Add(-1)
|
||||
t.Note().Step().Add(-1)
|
||||
case "OctaveAdd":
|
||||
t.Octave().Add(1)
|
||||
t.Note().Octave().Add(1)
|
||||
case "OctaveSubtract":
|
||||
t.Octave().Add(-1)
|
||||
t.Note().Octave().Add(-1)
|
||||
// Other miscellaneous
|
||||
case "Paste":
|
||||
gtx.Execute(clipboard.ReadCmd{Tag: t})
|
||||
case "OrderEditorFocus":
|
||||
t.InstrEnlarged().SetValue(false)
|
||||
t.Play().TrackerHidden().SetValue(false)
|
||||
gtx.Execute(key.FocusCmd{Tag: t.OrderEditor.scrollTable})
|
||||
case "TrackEditorFocus":
|
||||
t.InstrEnlarged().SetValue(false)
|
||||
t.Play().TrackerHidden().SetValue(false)
|
||||
gtx.Execute(key.FocusCmd{Tag: t.TrackEditor.scrollTable})
|
||||
case "InstrumentListFocus":
|
||||
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 {
|
||||
break
|
||||
}
|
||||
instr := t.Model.Instruments().Selected()
|
||||
n := noteAsValue(t.Model.Octave().Value(), val-12)
|
||||
instr := t.Model.Instrument().List().Selected()
|
||||
n := noteAsValue(t.Model.Note().Octave().Value(), val-12)
|
||||
t.KeyNoteMap.Press(e.Name, tracker.NoteEvent{Channel: instr, Note: n})
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,9 +92,9 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
||||
UniqueBtn: new(Clickable),
|
||||
TrackMidiInBtn: new(Clickable),
|
||||
scrollTable: NewScrollTable(
|
||||
model.Notes().Table(),
|
||||
model.Tracks(),
|
||||
model.NoteRows(),
|
||||
model.Note().Table(),
|
||||
model.Track().List(),
|
||||
model.Note().RowList(),
|
||||
),
|
||||
}
|
||||
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 {
|
||||
ev := t.noteEvents[0]
|
||||
ev.IsTrack = true
|
||||
ev.Channel = t.Model.Notes().Cursor().X
|
||||
ev.Channel = t.Model.Note().Cursor().X
|
||||
ev.Source = te
|
||||
if ev.On {
|
||||
t.Model.Notes().Input(ev.Note)
|
||||
t.Model.Note().Input(ev.Note)
|
||||
}
|
||||
copy(t.noteEvents, 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 {
|
||||
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")
|
||||
subtractSemitoneBtn := ActionBtn(t.SubtractSemitone(), t.Theme, te.SubtractSemitoneBtn, "-1", "Subtract semitone")
|
||||
addOctaveBtn := ActionBtn(t.AddOctave(), t.Theme, te.AddOctaveBtn, "+12", "Add octave")
|
||||
subtractOctaveBtn := ActionBtn(t.SubtractOctave(), t.Theme, te.SubtractOctaveBtn, "-12", "Subtract octave")
|
||||
noteOffBtn := ActionBtn(t.EditNoteOff(), t.Theme, te.NoteOffBtn, "Note Off", "")
|
||||
deleteTrackBtn := ActionIconBtn(t.DeleteTrack(), t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint)
|
||||
splitTrackBtn := ActionIconBtn(t.SplitTrack(), t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint)
|
||||
newTrackBtn := ActionIconBtn(t.AddTrack(), t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
|
||||
trackVoices := NumUpDown(t.Model.TrackVoices(), t.Theme, te.TrackVoices, "Track voices")
|
||||
addSemitoneBtn := ActionBtn(t.Note().AddSemitone(), t.Theme, te.AddSemitoneBtn, "+1", "Add semitone")
|
||||
subtractSemitoneBtn := ActionBtn(t.Note().SubtractSemitone(), t.Theme, te.SubtractSemitoneBtn, "-1", "Subtract semitone")
|
||||
addOctaveBtn := ActionBtn(t.Note().AddOctave(), t.Theme, te.AddOctaveBtn, "+12", "Add octave")
|
||||
subtractOctaveBtn := ActionBtn(t.Note().SubtractOctave(), t.Theme, te.SubtractOctaveBtn, "-12", "Subtract octave")
|
||||
noteOffBtn := ActionBtn(t.Note().NoteOff(), t.Theme, te.NoteOffBtn, "Note Off", "")
|
||||
deleteTrackBtn := ActionIconBtn(t.Track().Delete(), t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint)
|
||||
splitTrackBtn := ActionIconBtn(t.Track().Split(), t.Theme, te.SplitTrackBtn, icons.CommunicationCallSplit, te.splitTrackHint)
|
||||
newTrackBtn := ActionIconBtn(t.Track().Add(), t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
|
||||
trackVoices := NumUpDown(t.Model.Track().Voices(), t.Theme, te.TrackVoices, "Track voices")
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
trackVoicesInsetted := func(gtx C) D {
|
||||
return in.Layout(gtx, trackVoices.Layout)
|
||||
}
|
||||
effectBtn := ToggleBtn(t.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)
|
||||
midiInBtn := ToggleBtn(t.TrackMidiIn(), t.Theme, te.TrackMidiInBtn, "MIDI", "Input notes from MIDI keyboard")
|
||||
effectBtn := ToggleBtn(t.Track().Effect(), t.Theme, te.EffectBtn, "Hex", "Input notes as hex values")
|
||||
uniqueBtn := ToggleIconBtn(t.Note().UniquePatterns(), t.Theme, te.UniqueBtn, icons.ToggleStarBorder, icons.ToggleStar, te.uniqueOffTip, te.uniqueOnTip)
|
||||
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,
|
||||
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
|
||||
layout.Rigid(addSemitoneBtn.Layout),
|
||||
@ -220,13 +220,13 @@ var notes = []string{
|
||||
func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
||||
|
||||
beatMarkerDensity := t.RowsPerBeat().Value()
|
||||
beatMarkerDensity := t.Song().RowsPerBeat().Value()
|
||||
switch beatMarkerDensity {
|
||||
case 0, 1, 2:
|
||||
beatMarkerDensity = 4
|
||||
}
|
||||
|
||||
playSongRow := t.PlaySongRow()
|
||||
playSongRow := t.Play().SongRow()
|
||||
pxWidth := gtx.Dp(trackColWidth)
|
||||
pxHeight := gtx.Dp(trackRowHeight)
|
||||
pxPatMarkWidth := gtx.Dp(trackPatMarkWidth)
|
||||
@ -235,7 +235,7 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
colTitle := func(gtx C, i int) D {
|
||||
h := gtx.Dp(trackColTitleHeight)
|
||||
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)}
|
||||
}
|
||||
|
||||
@ -245,7 +245,7 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
} 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())
|
||||
}
|
||||
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())
|
||||
}
|
||||
return D{}
|
||||
@ -256,14 +256,14 @@ func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
patternRowOp := colorOp(gtx, t.Theme.NoteEditor.PatternRow.Color)
|
||||
|
||||
rowTitle := func(gtx C, j int) D {
|
||||
rpp := max(t.RowsPerPattern().Value(), 1)
|
||||
rpp := max(t.Song().RowsPerPattern().Value(), 1)
|
||||
pat := j / rpp
|
||||
row := j % rpp
|
||||
w := pxPatMarkWidth + pxRowMarkWidth
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
if row == 0 {
|
||||
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
|
||||
}
|
||||
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()
|
||||
drawSelection := cursor != te.scrollTable.Table.Cursor2()
|
||||
selection := te.scrollTable.Table.Range()
|
||||
hasTrackMidiIn := t.Model.TrackMidiIn().Value()
|
||||
hasTrackMidiIn := t.MIDI().InputtingNotes().Value()
|
||||
|
||||
patternNoOp := colorOp(gtx, t.Theme.NoteEditor.PatternNo.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
|
||||
rpp := max(t.RowsPerPattern().Value(), 1)
|
||||
rpp := max(t.Song().RowsPerPattern().Value(), 1)
|
||||
pat := y / rpp
|
||||
row := y % rpp
|
||||
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
|
||||
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)
|
||||
}
|
||||
op := noteOp
|
||||
val := noteName[byte(t.Model.Notes().Value(tracker.Point{X: x, Y: y}))]
|
||||
if t.Model.Notes().Effect(x) {
|
||||
val = noteHex[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.Track().Item(x).Effect {
|
||||
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)
|
||||
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) {
|
||||
cw := gtx.Constraints.Min.X
|
||||
cx := 0
|
||||
if t.Model.Notes().Effect(x) {
|
||||
if t.Model.Track().Item(x).Effect {
|
||||
cw /= 2
|
||||
if t.Model.Notes().LowNibble() {
|
||||
if t.Model.Note().LowNibble() {
|
||||
cx += cw
|
||||
}
|
||||
}
|
||||
@ -373,9 +373,9 @@ func noteAsValue(octave, note int) byte {
|
||||
|
||||
func (te *NoteEditor) command(t *Tracker, e key.Event) {
|
||||
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 {
|
||||
ev := t.Model.Notes().InputNibble(byte(nibbleValue))
|
||||
ev := t.Model.Note().InputNibble(byte(nibbleValue))
|
||||
t.KeyNoteMap.Press(e.Name, ev)
|
||||
}
|
||||
} else {
|
||||
@ -384,7 +384,7 @@ func (te *NoteEditor) command(t *Tracker, e key.Event) {
|
||||
return
|
||||
}
|
||||
if action == "NoteOff" {
|
||||
ev := t.Model.Notes().Input(0)
|
||||
ev := t.Model.Note().Input(0)
|
||||
t.KeyNoteMap.Press(e.Name, ev)
|
||||
return
|
||||
}
|
||||
@ -393,8 +393,8 @@ func (te *NoteEditor) command(t *Tracker, e key.Event) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n = noteAsValue(t.Octave().Value(), val-12)
|
||||
ev := t.Model.Notes().Input(n)
|
||||
n = noteAsValue(t.Note().Octave().Value(), val-12)
|
||||
ev := t.Model.Note().Input(n)
|
||||
t.KeyNoteMap.Press(e.Name, ev)
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,8 +42,8 @@ func NewOrderEditor(m *tracker.Model) *OrderEditor {
|
||||
return &OrderEditor{
|
||||
scrollTable: NewScrollTable(
|
||||
m.Order().Table(),
|
||||
m.Tracks(),
|
||||
m.OrderRows(),
|
||||
m.Track().List(),
|
||||
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.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))
|
||||
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)}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
return D{}
|
||||
@ -84,7 +84,7 @@ func (oe *OrderEditor) Layout(gtx C) D {
|
||||
rowTitle := func(gtx C, j int) D {
|
||||
w := gtx.Dp(unit.Dp(30))
|
||||
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
|
||||
}
|
||||
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 {
|
||||
case key.NameDeleteBackward:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Model.DeleteOrderRow(true).Do()
|
||||
t.Model.Order().DeleteRow(true).Do()
|
||||
}
|
||||
case key.NameDeleteForward:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Model.DeleteOrderRow(false).Do()
|
||||
t.Model.Order().DeleteRow(false).Do()
|
||||
}
|
||||
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 {
|
||||
t.Model.Order().SetValue(oe.scrollTable.Table.Cursor(), iv)
|
||||
|
||||
@ -6,7 +6,6 @@ import (
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/unit"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -20,12 +19,11 @@ type (
|
||||
|
||||
Oscilloscope struct {
|
||||
Theme *Theme
|
||||
Model *tracker.ScopeModel
|
||||
State *OscilloscopeState
|
||||
}
|
||||
)
|
||||
|
||||
func NewOscilloscope(model *tracker.Model) *OscilloscopeState {
|
||||
func NewOscilloscope() *OscilloscopeState {
|
||||
return &OscilloscopeState{
|
||||
plot: NewPlot(plotRange{0, 1}, plotRange{-1, 1}, 0),
|
||||
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{
|
||||
Theme: th,
|
||||
Model: m,
|
||||
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
|
||||
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout
|
||||
|
||||
triggerChannel := NumUpDown(s.Model.TriggerChannel(), s.Theme, s.State.triggerChannelNumber, "Trigger channel")
|
||||
lengthInBeats := NumUpDown(s.Model.LengthInBeats(), s.Theme, s.State.lengthInBeatsNumber, "Buffer length in beats")
|
||||
triggerChannel := NumUpDown(t.Scope().TriggerChannel(), s.Theme, s.State.triggerChannelNumber, "Trigger channel")
|
||||
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")
|
||||
wrapBtn := ToggleBtn(s.Model.Wrap(), s.Theme, s.State.wrapBtn, "Wrap", "Wrap buffer when full")
|
||||
onceBtn := ToggleBtn(t.Scope().Once(), s.Theme, s.State.onceBtn, "Once", "Trigger once on next event")
|
||||
wrapBtn := ToggleBtn(t.Scope().Wrap(), s.Theme, s.State.wrapBtn, "Wrap", "Wrap buffer when full")
|
||||
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
w := s.Model.Waveform()
|
||||
w := t.Scope().Waveform()
|
||||
cx := float32(w.Cursor) / float32(len(w.Buffer))
|
||||
|
||||
data := func(chn int, xr plotRange) (yr plotRange, ok bool) {
|
||||
@ -65,9 +62,10 @@ func (s *Oscilloscope) Layout(gtx C) D {
|
||||
if x1 > x2 {
|
||||
return plotRange{}, false
|
||||
}
|
||||
step := max((x2-x1)/1000, 1) // if the range is too large, sample only ~ 1000 points
|
||||
y1 := 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]
|
||||
y1 = max(y1, sample)
|
||||
y2 = min(y2, sample)
|
||||
@ -75,9 +73,9 @@ func (s *Oscilloscope) Layout(gtx C) D {
|
||||
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)) {
|
||||
l := s.Model.LengthInBeats().Value() * rpb
|
||||
l := t.Scope().LengthInBeats().Value() * rpb
|
||||
a := max(int(math.Ceil(float64(r.a*float32(l)))), 0)
|
||||
b := min(int(math.Floor(float64(r.b*float32(l)))), l)
|
||||
step := 1
|
||||
|
||||
@ -128,9 +128,9 @@ func (p ParamWidget) Layout(gtx C) D {
|
||||
title := Label(p.Theme, &p.Theme.UnitEditor.Name, p.Parameter.Name())
|
||||
t := TrackerFromContext(gtx)
|
||||
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) {
|
||||
t.ChooseSendTarget(p.Parameter.UnitID(), port).Do()
|
||||
t.Params().ChooseSendTarget(p.Parameter.UnitID(), port).Do()
|
||||
}
|
||||
k := Port(p.Theme, p.State)
|
||||
return k.Layout(gtx)
|
||||
@ -144,7 +144,7 @@ func (p ParamWidget) Layout(gtx C) D {
|
||||
return s.Layout(gtx)
|
||||
case tracker.IDParameter:
|
||||
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)
|
||||
if p.Disabled {
|
||||
|
||||
@ -75,9 +75,9 @@ func (pp *PatchPanel) Layout(gtx C) D {
|
||||
tr := TrackerFromContext(gtx)
|
||||
bottom := func(gtx C) D {
|
||||
switch {
|
||||
case tr.InstrComment().Value():
|
||||
case tr.Instrument().Tab().Value() == int(tracker.InstrumentCommentTab):
|
||||
return pp.instrProps.layout(gtx)
|
||||
case tr.InstrPresets().Value():
|
||||
case tr.Instrument().Tab().Value() == int(tracker.InstrumentPresetsTab):
|
||||
return pp.instrPresets.layout(gtx)
|
||||
default: // editor
|
||||
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 {
|
||||
switch {
|
||||
case pp.InstrComment().Value():
|
||||
case pp.Instrument().Tab().Value() == int(tracker.InstrumentCommentTab):
|
||||
return pp.instrProps.Tags(level, yield)
|
||||
case pp.InstrPresets().Value():
|
||||
case pp.Instrument().Tab().Value() == int(tracker.InstrumentPresetsTab):
|
||||
return pp.instrPresets.Tags(level, yield)
|
||||
default: // editor
|
||||
return pp.instrEditor.Tags(level, yield)
|
||||
@ -143,18 +143,18 @@ func MakeInstrumentTools(m *tracker.Model) InstrumentTools {
|
||||
func (it *InstrumentTools) Layout(gtx C) D {
|
||||
t := TrackerFromContext(gtx)
|
||||
it.update(gtx, t)
|
||||
editorBtn := TabBtn(t.Model.InstrEditor(), t.Theme, it.EditorTab, "Editor", "")
|
||||
presetsBtn := TabBtn(t.Model.InstrPresets(), t.Theme, it.PresetsTab, "Presets", "")
|
||||
commentBtn := TabBtn(t.Model.InstrComment(), t.Theme, it.CommentTab, "Properties", "")
|
||||
octave := NumUpDown(t.Model.Octave(), t.Theme, t.OctaveNumberInput, "Octave")
|
||||
linkInstrTrackBtn := ToggleIconBtn(t.Model.LinkInstrTrack(), 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)
|
||||
addInstrumentBtn := ActionIconBtn(t.Model.AddInstrument(), t.Theme, it.newInstrumentBtn, icons.ContentAdd, it.addInstrumentHint)
|
||||
editorBtn := TabBtn(tracker.MakeBool((*editorTab)(t.Model)), t.Theme, it.EditorTab, "Editor", "")
|
||||
presetsBtn := TabBtn(tracker.MakeBool((*presetsTab)(t.Model)), t.Theme, it.PresetsTab, "Presets", "")
|
||||
commentBtn := TabBtn(tracker.MakeBool((*commentTab)(t.Model)), t.Theme, it.CommentTab, "Properties", "")
|
||||
octave := NumUpDown(t.Note().Octave(), t.Theme, t.OctaveNumberInput, "Octave")
|
||||
linkInstrTrackBtn := ToggleIconBtn(t.Track().LinkInstrument(), t.Theme, it.linkInstrTrackBtn, icons.NotificationSyncDisabled, icons.NotificationSync, it.linkDisabledHint, it.linkEnabledHint)
|
||||
instrEnlargedBtn := ToggleIconBtn(t.Play().TrackerHidden(), t.Theme, it.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, it.enlargeHint, it.shrinkHint)
|
||||
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")
|
||||
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")
|
||||
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 {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
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))})
|
||||
tr.Alerts().Add("Instrument copied to clipboard", tracker.Info)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
tr.SaveInstrument(writer)
|
||||
tr.Instrument().Write(writer)
|
||||
}
|
||||
for it.loadInstrumentBtn.Clicked(gtx) {
|
||||
reader, err := tr.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp")
|
||||
if err != nil {
|
||||
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 {
|
||||
return InstrumentList{
|
||||
instrumentDragList: NewDragList(model.Instruments(), layout.Horizontal),
|
||||
instrumentDragList: NewDragList(model.Instrument().List(), layout.Horizontal),
|
||||
nameEditor: NewEditor(true, true, text.Middle),
|
||||
}
|
||||
}
|
||||
@ -221,7 +253,7 @@ func (il *InstrumentList) Layout(gtx C) D {
|
||||
element := func(gtx C, i int) D {
|
||||
grabhandle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, strconv.Itoa(i+1))
|
||||
label := func(gtx C) D {
|
||||
name, level, mute, ok := t.Instrument(i)
|
||||
name, level, mute, ok := t.Instrument().Item(i)
|
||||
if !ok {
|
||||
labelStyle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, "")
|
||||
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}
|
||||
}
|
||||
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()
|
||||
}
|
||||
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()
|
||||
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 == "" {
|
||||
@ -280,9 +312,9 @@ func (il *InstrumentList) update(gtx C, t *Tracker) {
|
||||
case key.NameDownArrow:
|
||||
var tagged Tagged
|
||||
switch {
|
||||
case t.InstrComment().Value():
|
||||
case t.Instrument().Tab().Value() == int(tracker.InstrumentCommentTab):
|
||||
tagged = &t.PatchPanel.instrProps
|
||||
case t.InstrPresets().Value():
|
||||
case t.Instrument().Tab().Value() == int(tracker.InstrumentPresetsTab):
|
||||
tagged = &t.PatchPanel.instrPresets
|
||||
default: // editor
|
||||
tagged = &t.PatchPanel.instrEditor
|
||||
|
||||
@ -61,7 +61,7 @@ func NewSongPanel(tr *Tracker) *SongPanel {
|
||||
RowsPerBeat: NewNumericUpDownState(),
|
||||
Step: NewNumericUpDownState(),
|
||||
SongLength: NewNumericUpDownState(),
|
||||
Scope: NewOscilloscope(tr.Model),
|
||||
Scope: NewOscilloscope(),
|
||||
MenuBar: NewMenuBar(tr),
|
||||
PlayBar: NewPlayBar(),
|
||||
|
||||
@ -88,14 +88,14 @@ func NewSongPanel(tr *Tracker) *SongPanel {
|
||||
|
||||
func (s *SongPanel) Update(gtx C, t *Tracker) {
|
||||
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) {
|
||||
t.Model.Oversampling().SetValue(!t.Oversampling().Value())
|
||||
t.Model.Detector().Oversampling().SetValue(!t.Detector().Oversampling().Value())
|
||||
}
|
||||
for s.SynthBtn.Clicked(gtx) {
|
||||
r := t.Model.SyntherIndex().Range()
|
||||
t.Model.SyntherIndex().SetValue((t.SyntherIndex().Value()+1)%(r.Max-r.Min+1) + r.Min)
|
||||
r := t.Model.Play().SyntherIndex().Range()
|
||||
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())
|
||||
|
||||
var weightingTxt string
|
||||
switch tracker.WeightingType(tr.Model.DetectorWeighting().Value()) {
|
||||
switch tracker.WeightingType(tr.Model.Detector().Weighting().Value()) {
|
||||
case tracker.KWeighting:
|
||||
weightingTxt = "K-weight (LUFS)"
|
||||
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, "")
|
||||
|
||||
oversamplingTxt := "Sample peak"
|
||||
if tr.Model.Oversampling().Value() {
|
||||
if tr.Model.Detector().Oversampling().Value() {
|
||||
oversamplingTxt = "True peak"
|
||||
}
|
||||
oversamplingBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.OversamplingBtn, oversamplingTxt, "")
|
||||
|
||||
cpuSmallLabel := func(gtx C) D {
|
||||
var a [vm.MAX_THREADS]sointu.CPULoad
|
||||
c := tr.Model.CPULoad(a[:])
|
||||
c := tr.Play().CPULoad(a[:])
|
||||
if c < 1 {
|
||||
return D{}
|
||||
}
|
||||
@ -150,7 +150,7 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
||||
cpuEnlargedWidget := func(gtx C) D {
|
||||
var sb strings.Builder
|
||||
var a [vm.MAX_THREADS]sointu.CPULoad
|
||||
c := tr.Model.CPULoad(a[:])
|
||||
c := tr.Play().CPULoad(a[:])
|
||||
high := false
|
||||
for i := range c {
|
||||
if i > 0 {
|
||||
@ -169,35 +169,35 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
||||
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 {
|
||||
switch index {
|
||||
case 0:
|
||||
return t.SongSettingsExpander.Layout(gtx, tr.Theme, "Song",
|
||||
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 {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
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)
|
||||
}),
|
||||
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)
|
||||
}),
|
||||
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)
|
||||
}),
|
||||
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)
|
||||
}),
|
||||
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)
|
||||
}),
|
||||
)
|
||||
@ -214,25 +214,25 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
||||
case 2:
|
||||
return t.LoudnessExpander.Layout(gtx, tr.Theme, "Loudness",
|
||||
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)
|
||||
},
|
||||
func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx,
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
gtx.Constraints.Min.X = 0
|
||||
@ -244,23 +244,23 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
||||
case 3:
|
||||
return t.PeakExpander.Layout(gtx, tr.Theme, "Peaks",
|
||||
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)
|
||||
},
|
||||
func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx,
|
||||
// no need to show momentary peak, it does not have too much meaning
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
gtx.Constraints.Min.X = 0
|
||||
@ -270,7 +270,7 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
||||
},
|
||||
)
|
||||
case 4:
|
||||
scope := Scope(tr.Theme, tr.Model.SignalAnalyzer(), t.Scope)
|
||||
scope := Scope(tr.Theme, t.Scope)
|
||||
scopeScaleBar := func(gtx C) D {
|
||||
return t.ScopeScaleBar.Layout(gtx, scope.Layout)
|
||||
}
|
||||
@ -289,7 +289,7 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
||||
gtx.Constraints.Min = gtx.Constraints.Max
|
||||
dims := t.List.Layout(gtx, 7, listItem)
|
||||
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
|
||||
}
|
||||
|
||||
@ -478,9 +478,9 @@ func NewMenuBar(tr *Tracker) *MenuBar {
|
||||
PanicBtn: new(Clickable),
|
||||
panicHint: makeHint("Panic", " (%s)", "PanicToggle"),
|
||||
}
|
||||
for input := range tr.MIDI.InputDevices {
|
||||
for input := range tr.MIDI().InputDevices {
|
||||
ret.midiMenuItems = append(ret.midiMenuItems,
|
||||
MenuItem(tr.SelectMidiInput(input), input, "", icons.ImageControlPoint),
|
||||
MenuItem(tr.MIDI().Open(input), input, "", icons.ImageControlPoint),
|
||||
)
|
||||
}
|
||||
return ret
|
||||
@ -495,11 +495,11 @@ func (t *MenuBar) Layout(gtx C) D {
|
||||
fileBtn := MenuBtn(tr.Theme, &t.MenuStates[0], &t.Clickables[0], "File")
|
||||
fileFC := layout.Rigid(func(gtx C) D {
|
||||
items := [...]ActionMenuItem{
|
||||
MenuItem(tr.NewSong(), "New Song", keyActionMap["NewSong"], icons.ContentClear),
|
||||
MenuItem(tr.OpenSong(), "Open Song", keyActionMap["OpenSong"], icons.FileFolder),
|
||||
MenuItem(tr.SaveSong(), "Save Song", keyActionMap["SaveSong"], icons.ContentSave),
|
||||
MenuItem(tr.SaveSongAs(), "Save Song As...", keyActionMap["SaveSongAs"], icons.ContentSave),
|
||||
MenuItem(tr.Export(), "Export Wav...", keyActionMap["ExportWav"], icons.ImageAudiotrack),
|
||||
MenuItem(tr.Song().New(), "New Song", keyActionMap["NewSong"], icons.ContentClear),
|
||||
MenuItem(tr.Song().Open(), "Open Song", keyActionMap["OpenSong"], icons.FileFolder),
|
||||
MenuItem(tr.Song().Save(), "Save Song", keyActionMap["SaveSong"], icons.ContentSave),
|
||||
MenuItem(tr.Song().SaveAs(), "Save Song As...", keyActionMap["SaveSongAs"], icons.ContentSave),
|
||||
MenuItem(tr.Song().Export(), "Export Wav...", keyActionMap["ExportWav"], icons.ImageAudiotrack),
|
||||
MenuItem(tr.RequestQuit(), "Quit", keyActionMap["Quit"], icons.ActionExitToApp),
|
||||
}
|
||||
if !canQuit {
|
||||
@ -510,9 +510,9 @@ func (t *MenuBar) Layout(gtx C) D {
|
||||
editBtn := MenuBtn(tr.Theme, &t.MenuStates[1], &t.Clickables[1], "Edit")
|
||||
editFC := layout.Rigid(func(gtx C) D {
|
||||
return editBtn.Layout(gtx,
|
||||
MenuItem(tr.Undo(), "Undo", keyActionMap["Undo"], icons.ContentUndo),
|
||||
MenuItem(tr.Redo(), "Redo", keyActionMap["Redo"], icons.ContentRedo),
|
||||
MenuItem(tr.RemoveUnused(), "Remove unused data", keyActionMap["RemoveUnused"], icons.ImageCrop),
|
||||
MenuItem(tr.History().Undo(), "Undo", keyActionMap["Undo"], icons.ContentUndo),
|
||||
MenuItem(tr.History().Redo(), "Redo", keyActionMap["Redo"], icons.ContentRedo),
|
||||
MenuItem(tr.Order().RemoveUnusedPatterns(), "Remove unused data", keyActionMap["RemoveUnused"], icons.ImageCrop),
|
||||
)
|
||||
})
|
||||
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.ShowLicense(), "License", keyActionMap["ShowLicense"], icons.ActionCopyright))
|
||||
})
|
||||
panicBtn := ToggleIconBtn(tr.Panic(), tr.Theme, t.PanicBtn, icons.AlertErrorOutline, icons.AlertError, t.panicHint, t.panicHint)
|
||||
if tr.Panic().Value() {
|
||||
panicBtn := ToggleIconBtn(tr.Play().Panicked(), tr.Theme, t.PanicBtn, icons.AlertErrorOutline, icons.AlertError, t.panicHint, t.panicHint)
|
||||
if tr.Play().Panicked().Value() {
|
||||
panicBtn.Style = &tr.Theme.IconButton.Error
|
||||
}
|
||||
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 {
|
||||
tr := TrackerFromContext(gtx)
|
||||
playBtn := ToggleIconBtn(tr.Playing(), tr.Theme, pb.PlayingBtn, icons.AVPlayArrow, icons.AVStop, pb.playHint, pb.stopHint)
|
||||
rewindBtn := ActionIconBtn(tr.PlaySongStart(), tr.Theme, pb.RewindBtn, icons.AVFastRewind, pb.rewindHint)
|
||||
recordBtn := ToggleIconBtn(tr.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)
|
||||
loopBtn := ToggleIconBtn(tr.LoopToggle(), tr.Theme, pb.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, pb.loopOffHint, pb.loopOnHint)
|
||||
playBtn := ToggleIconBtn(tr.Play().Started(), tr.Theme, pb.PlayingBtn, icons.AVPlayArrow, icons.AVStop, pb.playHint, pb.stopHint)
|
||||
rewindBtn := ActionIconBtn(tr.Play().FromBeginning(), tr.Theme, pb.RewindBtn, icons.AVFastRewind, pb.rewindHint)
|
||||
recordBtn := ToggleIconBtn(tr.Play().IsRecording(), tr.Theme, pb.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, pb.recordHint, pb.stopRecordHint)
|
||||
followBtn := ToggleIconBtn(tr.Play().IsFollowing(), tr.Theme, pb.FollowBtn, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, pb.followOffHint, pb.followOnHint)
|
||||
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 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
|
||||
|
||||
var chnModeTxt string = "???"
|
||||
switch tracker.SpecChnMode(t.Model.SpecAnChannelsInt().Value()) {
|
||||
switch tracker.SpecChnMode(t.Model.Spectrum().Channels().Value()) {
|
||||
case tracker.SpecChnModeSum:
|
||||
chnModeTxt = "Sum"
|
||||
case tracker.SpecChnModeSeparate:
|
||||
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")
|
||||
speed := NumUpDown(t.Model.SpecAnSpeed(), t.Theme, s.speed, "Speed")
|
||||
speed := NumUpDown(t.Model.Spectrum().Speed(), t.Theme, s.speed, "Speed")
|
||||
|
||||
numchns := 0
|
||||
speclen := len(t.Model.Spectrum()[0])
|
||||
speclen := len(t.Model.Spectrum().Result()[0])
|
||||
if speclen > 0 {
|
||||
numchns = 1
|
||||
if len(t.Model.Spectrum()[1]) == speclen {
|
||||
if len(t.Model.Spectrum().Result()[1]) == speclen {
|
||||
numchns = 2
|
||||
}
|
||||
}
|
||||
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
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) {
|
||||
if chn == 2 {
|
||||
if xr.a >= 0 {
|
||||
@ -88,16 +88,16 @@ func (s *SpectrumState) Layout(gtx C) D {
|
||||
y2 := float32(math.Inf(+1))
|
||||
switch {
|
||||
case x2 <= x1+1 && x2 < speclen-1: // perform smoothstep interpolation when we are overlapping only a few bins
|
||||
l := t.Model.Spectrum()[chn][x1]
|
||||
r := t.Model.Spectrum()[chn][x1+1]
|
||||
l := t.Model.Spectrum().Result()[chn][x1]
|
||||
r := t.Model.Spectrum().Result()[chn][x1+1]
|
||||
y1 = smoothInterpolate(l, r, float32(f1))
|
||||
l = t.Model.Spectrum()[chn][x2]
|
||||
r = t.Model.Spectrum()[chn][x2+1]
|
||||
l = t.Model.Spectrum().Result()[chn][x2]
|
||||
r = t.Model.Spectrum().Result()[chn][x2+1]
|
||||
y2 = smoothInterpolate(l, r, float32(f2))
|
||||
y1, y2 = max(y1, y2), min(y1, y2)
|
||||
default:
|
||||
for i := x1; i <= x2; i++ {
|
||||
sample := t.Model.Spectrum()[chn][i]
|
||||
sample := t.Model.Spectrum().Result()[chn][i]
|
||||
y1 = max(y1, sample)
|
||||
y2 = min(y2, sample)
|
||||
}
|
||||
@ -210,8 +210,8 @@ func nextPowerOfTwo(v int) int {
|
||||
func (s *SpectrumState) Update(gtx C) {
|
||||
t := TrackerFromContext(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.speed.Update(gtx, t.Model.SpecAnSpeed())
|
||||
s.resolutionNumber.Update(gtx, t.Model.Spectrum().Resolution())
|
||||
s.speed.Update(gtx, t.Model.Spectrum().Speed())
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ func NewTracker(model *tracker.Model) *Tracker {
|
||||
|
||||
Model: model,
|
||||
|
||||
filePathString: model.FilePath(),
|
||||
filePathString: model.Song().FilePath(),
|
||||
}
|
||||
t.SongPanel = NewSongPanel(t)
|
||||
t.KeyNoteMap = MakeKeyboard[key.Name](model.Broker())
|
||||
@ -185,12 +185,12 @@ func (t *Tracker) Main() {
|
||||
}
|
||||
acks <- struct{}{}
|
||||
case <-recoveryTicker.C:
|
||||
t.SaveRecovery()
|
||||
t.History().SaveRecovery()
|
||||
}
|
||||
}
|
||||
}
|
||||
recoveryTicker.Stop()
|
||||
t.SaveRecovery()
|
||||
t.History().SaveRecovery()
|
||||
close(t.Broker().FinishedGUI)
|
||||
}
|
||||
|
||||
@ -226,7 +226,7 @@ func (t *Tracker) Layout(gtx layout.Context) {
|
||||
paint.Fill(gtx.Ops, t.Theme.Material.Bg)
|
||||
event.Op(gtx.Ops, t) // area for capturing scroll events
|
||||
|
||||
if t.InstrEnlarged().Value() {
|
||||
if t.Play().TrackerHidden().Value() {
|
||||
t.layoutTop(gtx)
|
||||
} else {
|
||||
t.VerticalSplit.Layout(gtx,
|
||||
@ -263,14 +263,14 @@ func (t *Tracker) Layout(gtx layout.Context) {
|
||||
case key.Event:
|
||||
t.KeyEvent(e, gtx)
|
||||
case transfer.DataEvent:
|
||||
t.ReadSong(e.Open())
|
||||
t.Song().Read(e.Open())
|
||||
}
|
||||
}
|
||||
// if no-one else handled the note events, we handle them here
|
||||
for len(t.noteEvents) > 0 {
|
||||
ev := t.noteEvents[0]
|
||||
ev.IsTrack = false
|
||||
ev.Channel = t.Model.Instruments().Selected()
|
||||
ev.Channel = t.Model.Instrument().List().Selected()
|
||||
ev.Source = t
|
||||
copy(t.noteEvents, t.noteEvents[1:])
|
||||
t.noteEvents = t.noteEvents[:len(t.noteEvents)-1]
|
||||
@ -285,49 +285,49 @@ func (t *Tracker) showDialog(gtx C) {
|
||||
switch t.Dialog() {
|
||||
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.",
|
||||
DialogBtn("Save", t.SaveSong()),
|
||||
DialogBtn("Don't save", t.DiscardSong()),
|
||||
DialogBtn("Cancel", t.Cancel()),
|
||||
DialogBtn("Save", t.Song().Save()),
|
||||
DialogBtn("Don't save", t.Song().Discard()),
|
||||
DialogBtn("Cancel", t.CancelDialog()),
|
||||
)
|
||||
dialog.Layout(gtx)
|
||||
case tracker.Export:
|
||||
dialog := MakeDialog(t.Theme, t.DialogState, "Export format", "Choose the sample format for the exported .wav file.",
|
||||
DialogBtn("Int16", t.ExportInt16()),
|
||||
DialogBtn("Float32", t.ExportFloat()),
|
||||
DialogBtn("Cancel", t.Cancel()),
|
||||
DialogBtn("Int16", t.Song().ExportInt16()),
|
||||
DialogBtn("Float32", t.Song().ExportFloat()),
|
||||
DialogBtn("Cancel", t.CancelDialog()),
|
||||
)
|
||||
dialog.Layout(gtx)
|
||||
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:
|
||||
filename := t.filePathString.Value()
|
||||
if filename == "" {
|
||||
filename = "song.yml"
|
||||
}
|
||||
t.explorerCreateFile(t.WriteSong, filename)
|
||||
t.explorerCreateFile(t.Song().Write, filename)
|
||||
case tracker.ExportFloatExplorer, tracker.ExportInt16Explorer:
|
||||
filename := "song.wav"
|
||||
if p := t.filePathString.Value(); p != "" {
|
||||
filename = p[:len(p)-len(filepath.Ext(p))] + ".wav"
|
||||
}
|
||||
t.explorerCreateFile(func(wc io.WriteCloser) {
|
||||
t.WriteWav(wc, t.Dialog() == tracker.ExportInt16Explorer)
|
||||
t.Song().WriteWav(wc, t.Dialog() == tracker.ExportInt16Explorer)
|
||||
}, filename)
|
||||
case tracker.License:
|
||||
dialog := MakeDialog(t.Theme, t.DialogState, "License", sointu.License,
|
||||
DialogBtn("Close", t.Cancel()),
|
||||
DialogBtn("Close", t.CancelDialog()),
|
||||
)
|
||||
dialog.Layout(gtx)
|
||||
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.",
|
||||
DialogBtn("Delete", t.DeleteUserPreset()),
|
||||
DialogBtn("Cancel", t.Cancel()),
|
||||
DialogBtn("Delete", t.Preset().ConfirmDelete()),
|
||||
DialogBtn("Cancel", t.CancelDialog()),
|
||||
)
|
||||
dialog.Layout(gtx)
|
||||
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?",
|
||||
DialogBtn("Save", t.OverwriteUserPreset()),
|
||||
DialogBtn("Cancel", t.Cancel()),
|
||||
DialogBtn("Save", t.Preset().Overwrite()),
|
||||
DialogBtn("Cancel", t.CancelDialog()),
|
||||
)
|
||||
dialog.Layout(gtx)
|
||||
}
|
||||
@ -342,7 +342,7 @@ func (t *Tracker) explorerChooseFile(success func(io.ReadCloser), extensions ...
|
||||
if err == nil {
|
||||
success(file)
|
||||
} else {
|
||||
t.Cancel().Do()
|
||||
t.CancelDialog().Do()
|
||||
if err != explorer.ErrUserDecline {
|
||||
t.Alerts().Add(err.Error(), tracker.Error)
|
||||
}
|
||||
@ -360,7 +360,7 @@ func (t *Tracker) explorerCreateFile(success func(io.WriteCloser), filename stri
|
||||
if err == nil {
|
||||
success(file)
|
||||
} else {
|
||||
t.Cancel().Do()
|
||||
t.CancelDialog().Do()
|
||||
if err != explorer.ErrUserDecline {
|
||||
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 {
|
||||
ret := t.PatchPanel.Tags(curLevel+1, yield)
|
||||
if !t.InstrEnlarged().Value() {
|
||||
if !t.Play().TrackerHidden().Value() {
|
||||
ret = ret && t.OrderEditor.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 (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
@ -45,7 +42,7 @@ type (
|
||||
d modelData
|
||||
derived derivedModelData
|
||||
|
||||
instrEnlarged bool
|
||||
trackerHidden bool
|
||||
|
||||
prevUndoKind string
|
||||
undoSkipCounter int
|
||||
@ -71,7 +68,7 @@ type (
|
||||
|
||||
playerStatus PlayerStatus
|
||||
|
||||
signalAnalyzer *ScopeModel
|
||||
scopeData scopeData
|
||||
detectorResult DetectorResult
|
||||
|
||||
spectrum *Spectrum
|
||||
@ -79,7 +76,7 @@ type (
|
||||
weightingType WeightingType
|
||||
oversampling bool
|
||||
|
||||
specAnSettings SpecAnSettings
|
||||
specAnSettings specAnSettings
|
||||
specAnEnabled bool
|
||||
|
||||
alerts []Alert
|
||||
@ -90,10 +87,9 @@ type (
|
||||
|
||||
broker *Broker
|
||||
|
||||
MIDI MIDIContext
|
||||
midi MIDIContext
|
||||
|
||||
presets Presets
|
||||
presetIndex int
|
||||
presetData presetData
|
||||
}
|
||||
|
||||
// Cursor identifies a row and a track in a song score.
|
||||
@ -126,15 +122,6 @@ type (
|
||||
|
||||
Dialog int
|
||||
|
||||
MIDIContext interface {
|
||||
InputDevices(yield func(string) bool)
|
||||
Open(name string) error
|
||||
Close()
|
||||
IsOpen() bool
|
||||
}
|
||||
|
||||
NullMIDIContext struct{}
|
||||
|
||||
InstrumentTab int
|
||||
)
|
||||
|
||||
@ -174,31 +161,25 @@ const (
|
||||
InstrumentEditorTab InstrumentTab = iota
|
||||
InstrumentPresetsTab
|
||||
InstrumentCommentTab
|
||||
NumInstrumentTabs
|
||||
)
|
||||
|
||||
const maxUndo = 64
|
||||
|
||||
func (m *Model) PlayPosition() sointu.SongPos { return m.playerStatus.SongPos }
|
||||
func (m *Model) Loop() Loop { return m.loop }
|
||||
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 }
|
||||
func (m *Model) Dialog() Dialog { return m.dialog }
|
||||
func (m *Model) Quitted() bool { return m.quitted }
|
||||
|
||||
// NewModelPlayer creates a new model and a player that communicates with it
|
||||
func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext, recoveryFilePath string) *Model {
|
||||
m := new(Model)
|
||||
m.synthers = synthers
|
||||
m.MIDI = midiContext
|
||||
m.midi = midiContext
|
||||
m.broker = broker
|
||||
m.d.Octave = 4
|
||||
m.linkInstrTrack = true
|
||||
m.d.RecoveryFilePath = recoveryFilePath
|
||||
m.spectrum = broker.GetSpectrum()
|
||||
m.resetSong()
|
||||
m.Song().reset()
|
||||
if recoveryFilePath != "" {
|
||||
if bytes2, err := os.ReadFile(m.d.RecoveryFilePath); err == nil {
|
||||
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
|
||||
m.signalAnalyzer = NewScopeModel(m.d.Song.BPM)
|
||||
m.scopeData = scopeData{lengthInBeats: 4}
|
||||
m.Scope().updateBufferLength()
|
||||
m.updateDeriveData(SongChange)
|
||||
m.presets.load()
|
||||
m.updateDerivedPresetSearch()
|
||||
m.presetData.load()
|
||||
m.Preset().updateCache()
|
||||
m.derived.searchResults = make([]string, 0, len(sointu.UnitNames))
|
||||
m.updateDerivedUnitSearch()
|
||||
m.Unit().updateDerivedUnitSearch()
|
||||
go runDetector(broker)
|
||||
go runSpecAnalyzer(broker)
|
||||
return m
|
||||
}
|
||||
|
||||
func FindMIDIDeviceByPrefix(c MIDIContext, prefix string) (input string, ok bool) {
|
||||
for input := range c.InputDevices {
|
||||
if strings.HasPrefix(input, prefix) {
|
||||
return input, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
func (m *Model) Close() {
|
||||
TrySend(m.broker.CloseDetector, struct{}{})
|
||||
TrySend(m.broker.CloseSpecAn, struct{}{})
|
||||
TimeoutReceive(m.broker.FinishedDetector, 3*time.Second)
|
||||
TimeoutReceive(m.broker.FinishedSpecAn, 3*time.Second)
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if m.changeLevel == 0 {
|
||||
m.changeType = NoChange
|
||||
@ -276,7 +293,7 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func(
|
||||
}
|
||||
if m.changeType&BPMChange != 0 {
|
||||
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 {
|
||||
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) {
|
||||
if msg.HasPanicPlayerStatus {
|
||||
m.playerStatus = msg.PlayerStatus
|
||||
@ -373,7 +331,7 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
||||
m.d.Cursor2.SongPos = msg.PlayerStatus.SongPos
|
||||
TrySend(m.broker.ToGUI, any(MsgToGUI{
|
||||
Kind: GUIMessageCenterOnRow,
|
||||
Param: m.PlaySongRow(),
|
||||
Param: m.Play().SongRow(),
|
||||
}))
|
||||
}
|
||||
m.panic = msg.Panic
|
||||
@ -382,10 +340,10 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
||||
m.detectorResult = msg.DetectorResult
|
||||
}
|
||||
if msg.TriggerChannel > 0 {
|
||||
m.signalAnalyzer.Trigger(msg.TriggerChannel)
|
||||
m.Scope().trigger(msg.TriggerChannel)
|
||||
}
|
||||
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
|
||||
}
|
||||
switch e := msg.Data.(type) {
|
||||
@ -402,13 +360,13 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
||||
defer m.change("Recording", SongChange, MajorChange)()
|
||||
m.d.Song.Score = score
|
||||
m.d.Song.BPM = int(e.BPM + 0.5)
|
||||
m.instrEnlarged = false
|
||||
m.trackerHidden = false
|
||||
case Alert:
|
||||
m.Alerts().AddAlert(e)
|
||||
case IsPlayingMsg:
|
||||
m.playing = e.bool
|
||||
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
|
||||
if m.specAnEnabled { // send buffers to spectrum analyzer only if it's enabled
|
||||
clone := m.broker.GetAudioBuffer()
|
||||
@ -426,12 +384,7 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) CPULoad(buf []sointu.CPULoad) int {
|
||||
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 (m *Model) Broker() *Broker { return m.broker }
|
||||
|
||||
func (d *modelData) Copy() modelData {
|
||||
ret := *d
|
||||
@ -439,20 +392,6 @@ func (d *modelData) Copy() modelData {
|
||||
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 {
|
||||
maxID := 0
|
||||
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) {
|
||||
// Ints
|
||||
s.IterateInt("InstrumentVoices", s.model.InstrumentVoices(), yield, seed)
|
||||
s.IterateInt("TrackVoices", s.model.TrackVoices(), yield, seed)
|
||||
s.IterateInt("SongLength", s.model.SongLength(), yield, seed)
|
||||
s.IterateInt("BPM", s.model.BPM(), yield, seed)
|
||||
s.IterateInt("RowsPerPattern", s.model.RowsPerPattern(), yield, seed)
|
||||
s.IterateInt("RowsPerBeat", s.model.RowsPerBeat(), yield, seed)
|
||||
s.IterateInt("Step", s.model.Step(), yield, seed)
|
||||
s.IterateInt("Octave", s.model.Octave(), yield, seed)
|
||||
s.IterateInt("InstrumentVoices", s.model.Instrument().Voices(), yield, seed)
|
||||
s.IterateInt("TrackVoices", s.model.Track().Voices(), yield, seed)
|
||||
s.IterateInt("SongLength", s.model.Song().Length(), yield, seed)
|
||||
s.IterateInt("BPM", s.model.Song().BPM(), yield, seed)
|
||||
s.IterateInt("RowsPerPattern", s.model.Song().RowsPerPattern(), yield, seed)
|
||||
s.IterateInt("RowsPerBeat", s.model.Song().RowsPerBeat(), yield, seed)
|
||||
s.IterateInt("Step", s.model.Note().Step(), yield, seed)
|
||||
s.IterateInt("Octave", s.model.Note().Octave(), yield, seed)
|
||||
// Lists
|
||||
s.IterateList("Instruments", s.model.Instruments(), yield, seed)
|
||||
s.IterateList("Units", s.model.Units(), yield, seed)
|
||||
s.IterateList("Tracks", s.model.Tracks(), yield, seed)
|
||||
s.IterateList("OrderRows", s.model.OrderRows(), yield, seed)
|
||||
s.IterateList("NoteRows", s.model.NoteRows(), yield, seed)
|
||||
s.IterateList("UnitSearchResults", s.model.SearchResults(), yield, seed)
|
||||
s.IterateList("PresetDirs", s.model.PresetDirList().List(), yield, seed)
|
||||
s.IterateList("PresetResults", s.model.PresetResultList().List(), yield, seed)
|
||||
s.IterateList("Instruments", s.model.Instrument().List(), yield, seed)
|
||||
s.IterateList("Units", s.model.Unit().List(), yield, seed)
|
||||
s.IterateList("Tracks", s.model.Track().List(), yield, seed)
|
||||
s.IterateList("OrderRows", s.model.Order().RowList(), yield, seed)
|
||||
s.IterateList("NoteRows", s.model.Note().RowList(), yield, seed)
|
||||
s.IterateList("UnitSearchResults", s.model.Unit().SearchResults(), yield, seed)
|
||||
s.IterateList("PresetDirs", s.model.Preset().DirList(), yield, seed)
|
||||
s.IterateList("PresetResults", s.model.Preset().SearchResultList(), yield, seed)
|
||||
// Bools
|
||||
s.IterateBool("Panic", s.model.Panic(), yield, seed)
|
||||
s.IterateBool("Recording", s.model.IsRecording(), yield, seed)
|
||||
s.IterateBool("Playing", s.model.Playing(), yield, seed)
|
||||
s.IterateBool("InstrEnlarged", s.model.InstrEnlarged(), yield, seed)
|
||||
s.IterateBool("Effect", s.model.Effect(), yield, seed)
|
||||
s.IterateBool("Follow", s.model.Follow(), yield, seed)
|
||||
s.IterateBool("UniquePatterns", s.model.UniquePatterns(), yield, seed)
|
||||
s.IterateBool("LinkInstrTrack", s.model.LinkInstrTrack(), yield, seed)
|
||||
s.IterateBool("Panic", s.model.Play().Panicked(), yield, seed)
|
||||
s.IterateBool("Recording", s.model.Play().IsRecording(), yield, seed)
|
||||
s.IterateBool("Playing", s.model.Play().Started(), yield, seed)
|
||||
s.IterateBool("InstrEnlarged", s.model.Play().TrackerHidden(), yield, seed)
|
||||
s.IterateBool("Effect", s.model.Track().Effect(), yield, seed)
|
||||
s.IterateBool("Follow", s.model.Play().IsFollowing(), yield, seed)
|
||||
s.IterateBool("UniquePatterns", s.model.Note().UniquePatterns(), yield, seed)
|
||||
s.IterateBool("LinkInstrTrack", s.model.Track().LinkInstrument(), yield, seed)
|
||||
// Strings
|
||||
s.IterateString("FilePath", s.model.FilePath(), yield, seed)
|
||||
s.IterateString("InstrumentName", s.model.InstrumentName(), yield, seed)
|
||||
s.IterateString("InstrumentComment", s.model.InstrumentComment(), yield, seed)
|
||||
s.IterateString("UnitSearchText", s.model.UnitSearch(), yield, seed)
|
||||
s.IterateString("FilePath", s.model.Song().FilePath(), yield, seed)
|
||||
s.IterateString("InstrumentName", s.model.Instrument().Name(), yield, seed)
|
||||
s.IterateString("InstrumentComment", s.model.Instrument().Comment(), yield, seed)
|
||||
s.IterateString("UnitSearchText", s.model.Unit().SearchTerm(), yield, seed)
|
||||
// Actions
|
||||
s.IterateAction("AddTrack", s.model.AddTrack(), yield, seed)
|
||||
s.IterateAction("DeleteTrack", s.model.DeleteTrack(), yield, seed)
|
||||
s.IterateAction("AddInstrument", s.model.AddInstrument(), yield, seed)
|
||||
s.IterateAction("DeleteInstrument", s.model.DeleteInstrument(), yield, seed)
|
||||
s.IterateAction("AddUnitAfter", s.model.AddUnit(false), yield, seed)
|
||||
s.IterateAction("AddUnitBefore", s.model.AddUnit(true), yield, seed)
|
||||
s.IterateAction("DeleteUnit", s.model.DeleteUnit(), yield, seed)
|
||||
s.IterateAction("ClearUnit", s.model.ClearUnit(), yield, seed)
|
||||
s.IterateAction("Undo", s.model.Undo(), yield, seed)
|
||||
s.IterateAction("Redo", s.model.Redo(), yield, seed)
|
||||
s.IterateAction("RemoveUnused", s.model.RemoveUnused(), yield, seed)
|
||||
s.IterateAction("AddSemitone", s.model.AddSemitone(), yield, seed)
|
||||
s.IterateAction("SubtractSemitone", s.model.SubtractSemitone(), yield, seed)
|
||||
s.IterateAction("AddOctave", s.model.AddOctave(), yield, seed)
|
||||
s.IterateAction("SubtractOctave", s.model.SubtractOctave(), yield, seed)
|
||||
s.IterateAction("EditNoteOff", s.model.EditNoteOff(), yield, seed)
|
||||
s.IterateAction("PlaySongStart", s.model.PlaySongStart(), yield, seed)
|
||||
s.IterateAction("AddOrderRowAfter", s.model.AddOrderRow(false), yield, seed)
|
||||
s.IterateAction("AddOrderRowBefore", s.model.AddOrderRow(true), yield, seed)
|
||||
s.IterateAction("DeleteOrderRowForward", s.model.DeleteOrderRow(false), yield, seed)
|
||||
s.IterateAction("DeleteOrderRowBackward", s.model.DeleteOrderRow(true), yield, seed)
|
||||
s.IterateAction("SplitInstrument", s.model.SplitInstrument(), yield, seed)
|
||||
s.IterateAction("SplitTrack", s.model.SplitTrack(), yield, seed)
|
||||
s.IterateAction("AddTrack", s.model.Track().Add(), yield, seed)
|
||||
s.IterateAction("DeleteTrack", s.model.Track().Delete(), yield, seed)
|
||||
s.IterateAction("AddInstrument", s.model.Instrument().Add(), yield, seed)
|
||||
s.IterateAction("DeleteInstrument", s.model.Instrument().Delete(), yield, seed)
|
||||
s.IterateAction("AddUnitAfter", s.model.Unit().Add(false), yield, seed)
|
||||
s.IterateAction("AddUnitBefore", s.model.Unit().Add(true), yield, seed)
|
||||
s.IterateAction("DeleteUnit", s.model.Unit().Delete(), yield, seed)
|
||||
s.IterateAction("ClearUnit", s.model.Unit().Clear(), yield, seed)
|
||||
s.IterateAction("Undo", s.model.History().Undo(), yield, seed)
|
||||
s.IterateAction("Redo", s.model.History().Redo(), yield, seed)
|
||||
s.IterateAction("RemoveUnusedPatterns", s.model.Order().RemoveUnusedPatterns(), yield, seed)
|
||||
s.IterateAction("AddSemitone", s.model.Note().AddSemitone(), yield, seed)
|
||||
s.IterateAction("SubtractSemitone", s.model.Note().SubtractSemitone(), yield, seed)
|
||||
s.IterateAction("AddOctave", s.model.Note().AddOctave(), yield, seed)
|
||||
s.IterateAction("SubtractOctave", s.model.Note().SubtractOctave(), yield, seed)
|
||||
s.IterateAction("EditNoteOff", s.model.Note().NoteOff(), yield, seed)
|
||||
s.IterateAction("PlaySongStart", s.model.Play().FromBeginning(), yield, seed)
|
||||
s.IterateAction("AddOrderRowAfter", s.model.Order().AddRow(false), yield, seed)
|
||||
s.IterateAction("AddOrderRowBefore", s.model.Order().AddRow(true), yield, seed)
|
||||
s.IterateAction("DeleteOrderRowForward", s.model.Order().DeleteRow(false), yield, seed)
|
||||
s.IterateAction("DeleteOrderRowBackward", s.model.Order().DeleteRow(true), yield, seed)
|
||||
s.IterateAction("SplitInstrument", s.model.Instrument().Split(), yield, seed)
|
||||
s.IterateAction("SplitTrack", s.model.Track().Split(), yield, seed)
|
||||
// Tables
|
||||
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
|
||||
if s.file != nil {
|
||||
yield("ReadSong", func(p string, t *testing.T) {
|
||||
reader := bytes.NewReader(s.file)
|
||||
readCloser := io.NopCloser(reader)
|
||||
s.model.ReadSong(readCloser)
|
||||
s.model.Song().Read(readCloser)
|
||||
})
|
||||
yield("LoadInstrument", func(p string, t *testing.T) {
|
||||
reader := bytes.NewReader(s.file)
|
||||
readCloser := io.NopCloser(reader)
|
||||
s.model.LoadInstrument(readCloser)
|
||||
s.model.Instrument().Read(readCloser)
|
||||
})
|
||||
}
|
||||
// File saving
|
||||
yield("WriteSong", func(p string, t *testing.T) {
|
||||
writer := bytes.NewBuffer(nil)
|
||||
writeCloser := &myWriteCloser{writer}
|
||||
s.model.WriteSong(writeCloser)
|
||||
s.model.Song().Write(writeCloser)
|
||||
s.file = writer.Bytes()
|
||||
})
|
||||
yield("SaveInstrument", func(p string, t *testing.T) {
|
||||
writer := bytes.NewBuffer(nil)
|
||||
writeCloser := &myWriteCloser{writer}
|
||||
s.model.SaveInstrument(writeCloser)
|
||||
s.model.Instrument().Write(writeCloser)
|
||||
s.file = writer.Bytes()
|
||||
})
|
||||
}
|
||||
@ -255,6 +255,7 @@ func FuzzModel(f *testing.F) {
|
||||
synthers := []sointu.Synther{vm.GoSynther{}}
|
||||
broker := tracker.NewBroker()
|
||||
model := tracker.NewModel(broker, synthers, tracker.NullMIDIContext{}, "")
|
||||
defer model.Close()
|
||||
player := tracker.NewPlayer(broker, synthers[0])
|
||||
buf := make([][2]float32, 2048)
|
||||
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"
|
||||
)
|
||||
|
||||
// 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 (
|
||||
// Parameter represents a parameter of a unit. To support polymorphism
|
||||
// without causing allocations, it has a vtable that defines the methods for
|
||||
@ -27,7 +249,7 @@ type (
|
||||
parameterVtable interface {
|
||||
Value(*Parameter) int
|
||||
SetValue(*Parameter, int) bool
|
||||
Range(*Parameter) IntRange
|
||||
Range(*Parameter) RangeInclusive
|
||||
Type(*Parameter) ParameterType
|
||||
Name(*Parameter) string
|
||||
Hint(*Parameter) ParameterHint
|
||||
@ -35,9 +257,6 @@ type (
|
||||
RoundToGrid(*Parameter, int, bool) int
|
||||
}
|
||||
|
||||
Params Model
|
||||
ParamVertList Model
|
||||
|
||||
// different parameter vtables to handle different types of parameters.
|
||||
// Casting struct{} to interface does not cause allocations.
|
||||
namedParameter struct{}
|
||||
@ -99,9 +318,9 @@ func (p *Parameter) Add(delta int, snapToGrid bool) bool {
|
||||
return p.SetValue(newVal)
|
||||
}
|
||||
|
||||
func (p *Parameter) Range() IntRange {
|
||||
func (p *Parameter) Range() RangeInclusive {
|
||||
if p.vtable == nil {
|
||||
return IntRange{}
|
||||
return RangeInclusive{}
|
||||
}
|
||||
return p.vtable.Range(p)
|
||||
}
|
||||
@ -145,161 +364,6 @@ func (p *Parameter) UnitID() int {
|
||||
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
|
||||
|
||||
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
|
||||
return true
|
||||
}
|
||||
func (n *namedParameter) Range(p *Parameter) IntRange {
|
||||
return IntRange{Min: p.up.MinValue, Max: p.up.MaxValue}
|
||||
func (n *namedParameter) Range(p *Parameter) RangeInclusive {
|
||||
return RangeInclusive{Min: p.up.MinValue, Max: p.up.MaxValue}
|
||||
}
|
||||
func (n *namedParameter) Type(p *Parameter) ParameterType {
|
||||
if p.up == nil || !p.up.CanSet {
|
||||
@ -353,6 +417,25 @@ func (n *namedParameter) Reset(p *Parameter) {
|
||||
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
|
||||
|
||||
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
|
||||
return true
|
||||
}
|
||||
func (g *gmDlsEntryParameter) Range(p *Parameter) IntRange {
|
||||
return IntRange{Min: 0, Max: len(GmDlsEntries)}
|
||||
func (g *gmDlsEntryParameter) Range(p *Parameter) RangeInclusive {
|
||||
return RangeInclusive{Min: 0, Max: len(GmDlsEntries)}
|
||||
}
|
||||
func (g *gmDlsEntryParameter) Type(p *Parameter) ParameterType {
|
||||
return IntegerParameter
|
||||
@ -429,11 +512,11 @@ func (d *delayTimeParameter) SetValue(p *Parameter, v int) bool {
|
||||
p.unit.VarArgs[p.index] = v
|
||||
return true
|
||||
}
|
||||
func (d *delayTimeParameter) Range(p *Parameter) IntRange {
|
||||
func (d *delayTimeParameter) Range(p *Parameter) RangeInclusive {
|
||||
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 {
|
||||
val := d.Value(p)
|
||||
@ -511,7 +594,9 @@ func (d *delayLinesParameter) SetValue(p *Parameter, v int) bool {
|
||||
p.unit.VarArgs = p.unit.VarArgs[:targetLines]
|
||||
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) Name(p *Parameter) string { return "delaylines" }
|
||||
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
|
||||
|
||||
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 {
|
||||
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)
|
||||
@ -543,7 +642,9 @@ func (r *reverbParameter) SetValue(p *Parameter, v int) bool {
|
||||
copy(p.unit.VarArgs, entry.varArgs)
|
||||
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) Name(p *Parameter) string { return "reverb" }
|
||||
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/clean_presets.go
|
||||
|
||||
//go:embed presets/*
|
||||
var instrumentPresetFS embed.FS
|
||||
// Preset returns a PresetModel, a view of the model used to manipulate
|
||||
// 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 (
|
||||
// GmDlsEntry is a single sample entry from the gm.dls file
|
||||
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
|
||||
presetData struct {
|
||||
presets []preset
|
||||
dirs []string
|
||||
presetIndex int
|
||||
|
||||
cache presetCache
|
||||
}
|
||||
|
||||
Preset struct {
|
||||
Directory string
|
||||
User bool
|
||||
NeedsGmDls bool
|
||||
Instr sointu.Instrument
|
||||
preset struct {
|
||||
dir string
|
||||
user bool
|
||||
needsGmDls bool
|
||||
instr sointu.Instrument
|
||||
}
|
||||
|
||||
Presets 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 {
|
||||
presetCache struct {
|
||||
dir string
|
||||
dirIndex int
|
||||
noGmDls bool
|
||||
kind PresetKindEnum
|
||||
kind presetKindEnum
|
||||
searchStrings []string
|
||||
results []Preset
|
||||
results []preset
|
||||
}
|
||||
|
||||
PresetKindEnum int
|
||||
presetKindEnum int
|
||||
)
|
||||
|
||||
const (
|
||||
BuiltinPresets PresetKindEnum = -1
|
||||
AllPresets PresetKindEnum = 0
|
||||
UserPresets PresetKindEnum = 1
|
||||
BuiltinPresets presetKindEnum = -1
|
||||
AllPresets presetKindEnum = 0
|
||||
UserPresets presetKindEnum = 1
|
||||
)
|
||||
|
||||
func (m *Model) updateDerivedPresetSearch() {
|
||||
func (m *PresetModel) updateCache() {
|
||||
// reset derived data, keeping the
|
||||
str := m.derived.presetSearch.searchStrings[:0]
|
||||
m.derived.presetSearch = derivedPresetSearch{searchStrings: str, dirIndex: -1}
|
||||
str := m.presetData.cache.searchStrings[:0]
|
||||
m.presetData.cache = presetCache{searchStrings: str, dirIndex: -1}
|
||||
// parse filters from the search string. in: dir, gmdls: yes/no, kind: builtin/user/all
|
||||
search := strings.TrimSpace(m.d.PresetSearchString)
|
||||
parts := strings.Fields(search)
|
||||
@ -95,69 +317,73 @@ func (m *Model) updateDerivedPresetSearch() {
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "d:") && len(part) > 2 {
|
||||
dir := strings.TrimSpace(part[2:])
|
||||
m.derived.presetSearch.dir = dir
|
||||
ind := slices.IndexFunc(m.presets.Dirs, func(c string) bool { return c == dir })
|
||||
m.derived.presetSearch.dirIndex = ind
|
||||
m.presetData.cache.dir = dir
|
||||
ind := slices.IndexFunc(m.presetData.dirs, func(c string) bool { return c == dir })
|
||||
m.presetData.cache.dirIndex = ind
|
||||
} 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 {
|
||||
val := strings.TrimSpace(part[2:3])
|
||||
switch val {
|
||||
case "b":
|
||||
m.derived.presetSearch.kind = BuiltinPresets
|
||||
m.presetData.cache.kind = BuiltinPresets
|
||||
case "u":
|
||||
m.derived.presetSearch.kind = UserPresets
|
||||
m.presetData.cache.kind = UserPresets
|
||||
}
|
||||
} 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
|
||||
m.derived.presetSearch.results = m.derived.presetSearch.results[:0]
|
||||
for _, p := range m.presets.Presets {
|
||||
if m.derived.presetSearch.kind == BuiltinPresets && p.User {
|
||||
m.presetData.cache.results = m.presetData.cache.results[:0]
|
||||
for _, p := range m.presetData.presets {
|
||||
if m.presetData.cache.kind == BuiltinPresets && p.user {
|
||||
continue
|
||||
}
|
||||
if m.derived.presetSearch.kind == UserPresets && !p.User {
|
||||
if m.presetData.cache.kind == UserPresets && !p.user {
|
||||
continue
|
||||
}
|
||||
if m.derived.presetSearch.dir != "" && p.Directory != m.derived.presetSearch.dir {
|
||||
if m.presetData.cache.dir != "" && p.dir != m.presetData.cache.dir {
|
||||
continue
|
||||
}
|
||||
if m.derived.presetSearch.noGmDls && p.NeedsGmDls {
|
||||
if m.presetData.cache.noGmDls && p.needsGmDls {
|
||||
continue
|
||||
}
|
||||
if len(m.derived.presetSearch.searchStrings) == 0 {
|
||||
if len(m.presetData.cache.searchStrings) == 0 {
|
||||
goto found
|
||||
}
|
||||
for _, s := range m.derived.presetSearch.searchStrings {
|
||||
if strings.Contains(strings.ToLower(p.Instr.Name), s) {
|
||||
for _, s := range m.presetData.cache.searchStrings {
|
||||
if strings.Contains(strings.ToLower(p.instr.Name), s) {
|
||||
goto found
|
||||
}
|
||||
}
|
||||
continue
|
||||
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() {
|
||||
*m = Presets{}
|
||||
//go:embed presets/*
|
||||
var builtInPresetsFS embed.FS
|
||||
|
||||
func (m *presetData) load() {
|
||||
m.dirs = m.dirs[:0]
|
||||
m.presets = m.presets[:0]
|
||||
seenDir := make(map[string]bool)
|
||||
m.loadPresetsFromFs(instrumentPresetFS, false, seenDir)
|
||||
m.loadPresetsFromFs(builtInPresetsFS, false, seenDir)
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
userPresets := filepath.Join(configDir, "sointu")
|
||||
m.loadPresetsFromFs(os.DirFS(userPresets), true, seenDir)
|
||||
}
|
||||
sort.Sort(m)
|
||||
m.Dirs = make([]string, 0, len(seenDir))
|
||||
m.dirs = make([]string, 0, len(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 {
|
||||
if err != nil {
|
||||
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
|
||||
instr.Name = filenameToInstrumentName(splitted[len(splitted)-1])
|
||||
dir := strings.Join(splitted[:len(splitted)-1], "/")
|
||||
preset := Preset{
|
||||
Directory: dir,
|
||||
User: userDefined,
|
||||
Instr: instr,
|
||||
NeedsGmDls: checkNeedsGmDls(instr),
|
||||
preset := preset{
|
||||
dir: dir,
|
||||
user: userDefined,
|
||||
instr: instr,
|
||||
needsGmDls: checkNeedsGmDls(instr),
|
||||
}
|
||||
if dir != "" {
|
||||
seenDir[dir] = true
|
||||
}
|
||||
m.Presets = append(m.Presets, preset)
|
||||
m.presets = append(m.presets, preset)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@ -217,125 +443,6 @@ func checkNeedsGmDls(instr sointu.Instrument) bool {
|
||||
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 {
|
||||
parts := strings.Split(str, " ")
|
||||
newParts := make([]string, 0, len(parts))
|
||||
@ -347,175 +454,6 @@ func removeFilters(str string, prefix string) string {
|
||||
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 {
|
||||
subPath := path
|
||||
var result []string
|
||||
@ -541,11 +479,11 @@ func splitPath(path string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
func (p Presets) Len() int { return len(p.Presets) }
|
||||
func (p Presets) Less(i, j int) bool {
|
||||
if p.Presets[i].Instr.Name == p.Presets[j].Instr.Name {
|
||||
return p.Presets[i].User && !p.Presets[j].User
|
||||
func (p presetData) Len() int { return len(p.presets) }
|
||||
func (p presetData) Less(i, j int) bool {
|
||||
if p.presets[i].instr.Name == p.presets[j].instr.Name {
|
||||
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"
|
||||
)
|
||||
|
||||
type (
|
||||
SpecAnalyzer struct {
|
||||
settings SpecAnSettings
|
||||
broker *Broker
|
||||
chunker chunker
|
||||
temp specTemp
|
||||
}
|
||||
// Spectrum returns a SpectrumModel to access spectrum analyzer data and
|
||||
// settings.
|
||||
func (m *Model) Spectrum() *SpectrumModel { return (*SpectrumModel)(m) }
|
||||
|
||||
SpecAnSettings struct {
|
||||
ChnMode SpecChnMode
|
||||
Smooth int
|
||||
Resolution int
|
||||
}
|
||||
type SpectrumModel Model
|
||||
|
||||
SpecChnMode int
|
||||
Spectrum [2][]float32
|
||||
// Result returns the latest spectrum analyzer result.
|
||||
func (m *SpectrumModel) Result() Spectrum { return *m.spectrum }
|
||||
|
||||
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
|
||||
}
|
||||
type Spectrum [2][]float32
|
||||
|
||||
BiquadCoeffs struct {
|
||||
b0, b1, b2 float32
|
||||
a0, a1, a2 float32
|
||||
}
|
||||
// Speed returns an Int to adjust the smoothing speed of the spectrum analyzer.
|
||||
func (m *SpectrumModel) Speed() Int { return MakeInt((*spectrumSpeed)(m)) }
|
||||
|
||||
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 (
|
||||
SpecResolutionMin = -3
|
||||
SpecResolutionMax = 3
|
||||
)
|
||||
|
||||
const (
|
||||
SpecSpeedMin = -3
|
||||
SpecSpeedMax = 3
|
||||
)
|
||||
// Channels returns an Int to adjust the channel mode of the spectrum analyzer.
|
||||
func (m *SpectrumModel) Channels() Int { return MakeInt((*spectrumChannels)(m)) }
|
||||
|
||||
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 (
|
||||
SpecChnModeSum SpecChnMode = iota // calculate a single combined spectrum for both channels
|
||||
@ -58,15 +80,14 @@ const (
|
||||
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 {
|
||||
ret := &SpecAnalyzer{broker: broker}
|
||||
ret.init(SpecAnSettings{})
|
||||
return ret
|
||||
}
|
||||
|
||||
func (m *Model) BiquadCoeffs() (coeffs BiquadCoeffs, ok bool) {
|
||||
// BiquadCoeffs returns the biquad filter coefficients of the currently selected
|
||||
// filter or belleq, to plot its frequency response on top of the spectrum.
|
||||
func (m *SpectrumModel) BiquadCoeffs() (coeffs BiquadCoeffs, ok bool) {
|
||||
i := m.d.InstrIndex
|
||||
u := m.d.UnitIndex
|
||||
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 {
|
||||
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)) /
|
||||
(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 {
|
||||
select {
|
||||
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 {
|
||||
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
|
||||
a.settings = s
|
||||
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()
|
||||
switch s.settings.ChnMode {
|
||||
case SpecChnModeSeparate:
|
||||
@ -220,7 +272,7 @@ func (s *SpecAnalyzer) update(buf sointu.AudioBuffer) *Spectrum {
|
||||
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
|
||||
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