refactor(tracker): group Model methods, with each group in one source file

This commit is contained in:
5684185+vsariola@users.noreply.github.com
2026-01-25 13:08:45 +02:00
parent b93304adab
commit 86ca3fb300
44 changed files with 4813 additions and 4482 deletions

View File

@ -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))
}
}

View File

@ -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
View 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)
}

View File

@ -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) }

View File

@ -98,7 +98,7 @@ type (
}
MsgToSpecAn struct {
SpecSettings SpecAnSettings
SpecSettings specAnSettings
HasSettings bool
Data any
}

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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)

View File

@ -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})
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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,

View File

@ -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())
}

View File

@ -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
View 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
View 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
}

View File

@ -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
}

View File

@ -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
View 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 }

View File

@ -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 {

View File

@ -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
View 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
View 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
}
}

View File

@ -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
View 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])
}

View File

@ -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
View 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++
}
}

View File

@ -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
View 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()
}()
}

View File

@ -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])
}

View File

@ -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
}

View File

@ -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
View 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
View 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
View 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
}