From 86ca3fb3005fe5af0f90795c5243f65331a9f9d1 Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:08:45 +0200 Subject: [PATCH] refactor(tracker): group Model methods, with each group in one source file --- cmd/sointu-track/main.go | 12 +- cmd/sointu-vsti/main.go | 17 +- tracker/action.go | 675 ------------------- tracker/alert.go | 16 +- tracker/basic_types.go | 550 +++++++++++++++ tracker/bool.go | 382 ----------- tracker/broker.go | 2 +- tracker/derived.go | 47 -- tracker/detector.go | 152 +++-- tracker/doc.go | 19 + tracker/files.go | 190 ------ tracker/gioui/instrument_editor.go | 70 +- tracker/gioui/instrument_presets.go | 24 +- tracker/gioui/instrument_properties.go | 20 +- tracker/gioui/keybindings.go | 160 ++--- tracker/gioui/note_editor.go | 72 +- tracker/gioui/order_editor.go | 16 +- tracker/gioui/oscilloscope.go | 24 +- tracker/gioui/param.go | 6 +- tracker/gioui/patch_panel.go | 76 ++- tracker/gioui/song_panel.go | 92 +-- tracker/gioui/specanalyzer.go | 28 +- tracker/gioui/tracker.go | 46 +- tracker/history.go | 118 ++++ tracker/instrument.go | 483 ++++++++++++++ tracker/int.go | 255 ------- tracker/list.go | 890 ------------------------- tracker/midi.go | 77 +++ tracker/model.go | 191 ++---- tracker/model_test.go | 113 ++-- tracker/note.go | 427 ++++++++++++ tracker/order.go | 440 ++++++++++++ tracker/params.go | 441 +++++++----- tracker/play.go | 193 ++++++ tracker/presets.go | 688 +++++++++---------- tracker/scope.go | 127 ++++ tracker/scopemodel.go | 124 ---- tracker/song.go | 367 ++++++++++ tracker/spectrum.go | 140 ++-- tracker/string.go | 136 ---- tracker/table.go | 625 ----------------- tracker/track.go | 186 ++++++ tracker/unit.go | 387 +++++++++++ tracker/voices.go | 191 ++++++ 44 files changed, 4813 insertions(+), 4482 deletions(-) delete mode 100644 tracker/action.go create mode 100644 tracker/basic_types.go delete mode 100644 tracker/bool.go delete mode 100644 tracker/files.go create mode 100644 tracker/history.go create mode 100644 tracker/instrument.go delete mode 100644 tracker/int.go delete mode 100644 tracker/list.go create mode 100644 tracker/midi.go create mode 100644 tracker/note.go create mode 100644 tracker/order.go create mode 100644 tracker/play.go create mode 100644 tracker/scope.go delete mode 100644 tracker/scopemodel.go create mode 100644 tracker/song.go delete mode 100644 tracker/string.go delete mode 100644 tracker/table.go create mode 100644 tracker/track.go create mode 100644 tracker/unit.go create mode 100644 tracker/voices.go diff --git a/cmd/sointu-track/main.go b/cmd/sointu-track/main.go index add2561..b80a190 100644 --- a/cmd/sointu-track/main.go +++ b/cmd/sointu-track/main.go @@ -8,7 +8,6 @@ import ( "path/filepath" "runtime" "runtime/pprof" - "time" "gioui.org/app" "github.com/vsariola/sointu" @@ -60,15 +59,11 @@ func main() { } model := tracker.NewModel(broker, cmd.Synthers, midiContext, recoveryFile) player := tracker.NewPlayer(broker, cmd.Synthers[0]) - detector := tracker.NewDetector(broker) - specan := tracker.NewSpecAnalyzer(broker) - go detector.Run() - go specan.Run() if a := flag.Args(); len(a) > 0 { f, err := os.Open(a[0]) if err == nil { - model.ReadSong(f) + model.Song().Read(f) } f.Close() } @@ -82,10 +77,7 @@ func main() { go func() { trackerUi.Main() audioCloser.Close() - tracker.TrySend(broker.CloseDetector, struct{}{}) - tracker.TrySend(broker.CloseSpecAn, struct{}{}) - tracker.TimeoutReceive(broker.FinishedDetector, 3*time.Second) - tracker.TimeoutReceive(broker.FinishedSpecAn, 3*time.Second) + model.Close() if *cpuprofile != "" { pprof.StopCPUProfile() f.Close() diff --git a/cmd/sointu-vsti/main.go b/cmd/sointu-vsti/main.go index ba3336f..1a5d871 100644 --- a/cmd/sointu-vsti/main.go +++ b/cmd/sointu-vsti/main.go @@ -48,18 +48,14 @@ func init() { broker := tracker.NewBroker() model := tracker.NewModel(broker, cmd.Synthers, cmd.NewMidiContext(broker), recoveryFile) player := tracker.NewPlayer(broker, cmd.Synthers[0]) - detector := tracker.NewDetector(broker) - specan := tracker.NewSpecAnalyzer(broker) - go detector.Run() - go specan.Run() t := gioui.NewTracker(model) - model.InstrEnlarged().SetValue(true) + model.Play().TrackerHidden().SetValue(true) // since the VST is usually working without any regard for the tracks // until recording, disable the Instrument-Track linking by default // because it might just confuse the user why instrument cannot be // swapped/added etc. - model.LinkInstrTrack().SetValue(false) + model.Track().LinkInstrument().SetValue(false) go t.Main() context := &VSTIProcessContext{host: h} buf := make(sointu.AudioBuffer, 1024) @@ -112,24 +108,21 @@ func init() { } }, CloseFunc: func() { - tracker.TrySend(broker.CloseDetector, struct{}{}) tracker.TrySend(broker.CloseGUI, struct{}{}) - tracker.TrySend(broker.CloseSpecAn, struct{}{}) - tracker.TimeoutReceive(broker.FinishedDetector, 3*time.Second) + model.Close() tracker.TimeoutReceive(broker.FinishedGUI, 3*time.Second) - tracker.TimeoutReceive(broker.FinishedSpecAn, 3*time.Second) }, GetChunkFunc: func(isPreset bool) []byte { retChn := make(chan []byte) - if !tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { retChn <- t.MarshalRecovery() }}) { + if !tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { retChn <- t.History().MarshalRecovery() }}) { return nil } ret, _ := tracker.TimeoutReceive(retChn, 5*time.Second) // ret will be nil if timeout or channel closed return ret }, SetChunkFunc: func(data []byte, isPreset bool) { - tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { t.UnmarshalRecovery(data) }}) + tracker.TrySend(broker.ToModel, tracker.MsgToModel{Data: func() { t.History().UnmarshalRecovery(data) }}) }, } diff --git a/tracker/action.go b/tracker/action.go deleted file mode 100644 index f4319fe..0000000 --- a/tracker/action.go +++ /dev/null @@ -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)) - } -} diff --git a/tracker/alert.go b/tracker/alert.go index d5b6f96..6d25a3e 100644 --- a/tracker/alert.go +++ b/tracker/alert.go @@ -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 { diff --git a/tracker/basic_types.go b/tracker/basic_types.go new file mode 100644 index 0000000..65cef78 --- /dev/null +++ b/tracker/basic_types.go @@ -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) +} diff --git a/tracker/bool.go b/tracker/bool.go deleted file mode 100644 index 70b950a..0000000 --- a/tracker/bool.go +++ /dev/null @@ -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<= 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) } diff --git a/tracker/broker.go b/tracker/broker.go index e764109..1efe1aa 100644 --- a/tracker/broker.go +++ b/tracker/broker.go @@ -98,7 +98,7 @@ type ( } MsgToSpecAn struct { - SpecSettings SpecAnSettings + SpecSettings specAnSettings HasSettings bool Data any } diff --git a/tracker/derived.go b/tracker/derived.go index 7a47ba6..103e580 100644 --- a/tracker/derived.go +++ b/tracker/derived.go @@ -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) { diff --git a/tracker/detector.go b/tracker/detector.go index 99d1abf..62ba0da 100644 --- a/tracker/detector.go +++ b/tracker/detector.go @@ -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 diff --git a/tracker/doc.go b/tracker/doc.go index 1532c69..4236c57 100644 --- a/tracker/doc.go +++ b/tracker/doc.go @@ -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 diff --git a/tracker/files.go b/tracker/files.go deleted file mode 100644 index 0d1c4d3..0000000 --- a/tracker/files.go +++ /dev/null @@ -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 -} diff --git a/tracker/gioui/instrument_editor.go b/tracker/gioui/instrument_editor.go index af3687c..6d31aea 100644 --- a/tracker/gioui/instrument_editor.go +++ b/tracker/gioui/instrument_editor.go @@ -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) diff --git a/tracker/gioui/instrument_presets.go b/tracker/gioui/instrument_presets.go index e517bb5..c194617 100644 --- a/tracker/gioui/instrument_presets.go +++ b/tracker/gioui/instrument_presets.go @@ -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 { diff --git a/tracker/gioui/instrument_properties.go b/tracker/gioui/instrument_properties.go index a544033..3855969 100644 --- a/tracker/gioui/instrument_properties.go +++ b/tracker/gioui/instrument_properties.go @@ -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) diff --git a/tracker/gioui/keybindings.go b/tracker/gioui/keybindings.go index ee46d48..facf5c5 100644 --- a/tracker/gioui/keybindings.go +++ b/tracker/gioui/keybindings.go @@ -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}) } } diff --git a/tracker/gioui/note_editor.go b/tracker/gioui/note_editor.go index 8a5572b..baeb5c3 100644 --- a/tracker/gioui/note_editor.go +++ b/tracker/gioui/note_editor.go @@ -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) } } diff --git a/tracker/gioui/order_editor.go b/tracker/gioui/order_editor.go index 04398f3..20145fe 100644 --- a/tracker/gioui/order_editor.go +++ b/tracker/gioui/order_editor.go @@ -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) diff --git a/tracker/gioui/oscilloscope.go b/tracker/gioui/oscilloscope.go index 0f3917d..551c9ec 100644 --- a/tracker/gioui/oscilloscope.go +++ b/tracker/gioui/oscilloscope.go @@ -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 diff --git a/tracker/gioui/param.go b/tracker/gioui/param.go index 0498ca9..846eddc 100644 --- a/tracker/gioui/param.go +++ b/tracker/gioui/param.go @@ -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 { diff --git a/tracker/gioui/patch_panel.go b/tracker/gioui/patch_panel.go index 6ebc81e..671141d 100644 --- a/tracker/gioui/patch_panel.go +++ b/tracker/gioui/patch_panel.go @@ -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 diff --git a/tracker/gioui/song_panel.go b/tracker/gioui/song_panel.go index a0d579b..c73759e 100644 --- a/tracker/gioui/song_panel.go +++ b/tracker/gioui/song_panel.go @@ -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, diff --git a/tracker/gioui/specanalyzer.go b/tracker/gioui/specanalyzer.go index 5f370dd..621c5eb 100644 --- a/tracker/gioui/specanalyzer.go +++ b/tracker/gioui/specanalyzer.go @@ -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()) } diff --git a/tracker/gioui/tracker.go b/tracker/gioui/tracker.go index 1b7758b..d518b2d 100644 --- a/tracker/gioui/tracker.go +++ b/tracker/gioui/tracker.go @@ -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) } diff --git a/tracker/history.go b/tracker/history.go new file mode 100644 index 0000000..5cc8744 --- /dev/null +++ b/tracker/history.go @@ -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) +} diff --git a/tracker/instrument.go b/tracker/instrument.go new file mode 100644 index 0000000..e272378 --- /dev/null +++ b/tracker/instrument.go @@ -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<= 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 +} diff --git a/tracker/int.go b/tracker/int.go deleted file mode 100644 index a96eadd..0000000 --- a/tracker/int.go +++ /dev/null @@ -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 -} diff --git a/tracker/list.go b/tracker/list.go deleted file mode 100644 index a7771f8..0000000 --- a/tracker/list.go +++ /dev/null @@ -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 -} diff --git a/tracker/midi.go b/tracker/midi.go new file mode 100644 index 0000000..45788e8 --- /dev/null +++ b/tracker/midi.go @@ -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 } diff --git a/tracker/model.go b/tracker/model.go index ff9e91b..0933859 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -2,11 +2,8 @@ package tracker import ( "encoding/json" - "errors" - "fmt" "os" - "path/filepath" - "strings" + "time" "github.com/vsariola/sointu" ) @@ -45,7 +42,7 @@ type ( d modelData derived derivedModelData - instrEnlarged bool + trackerHidden bool prevUndoKind string undoSkipCounter int @@ -71,7 +68,7 @@ type ( playerStatus PlayerStatus - signalAnalyzer *ScopeModel + scopeData scopeData detectorResult DetectorResult spectrum *Spectrum @@ -79,7 +76,7 @@ type ( weightingType WeightingType oversampling bool - specAnSettings SpecAnSettings + specAnSettings specAnSettings specAnEnabled bool alerts []Alert @@ -90,10 +87,9 @@ type ( broker *Broker - MIDI MIDIContext + midi MIDIContext - presets Presets - presetIndex int + presetData presetData } // Cursor identifies a row and a track in a song score. @@ -126,15 +122,6 @@ type ( Dialog int - MIDIContext interface { - InputDevices(yield func(string) bool) - Open(name string) error - Close() - IsOpen() bool - } - - NullMIDIContext struct{} - InstrumentTab int ) @@ -174,31 +161,25 @@ const ( InstrumentEditorTab InstrumentTab = iota InstrumentPresetsTab InstrumentCommentTab + NumInstrumentTabs ) const maxUndo = 64 -func (m *Model) PlayPosition() sointu.SongPos { return m.playerStatus.SongPos } -func (m *Model) Loop() Loop { return m.loop } -func (m *Model) PlaySongRow() int { return m.d.Song.Score.SongRow(m.playerStatus.SongPos) } -func (m *Model) ChangedSinceSave() bool { return m.d.ChangedSinceSave } -func (m *Model) Dialog() Dialog { return m.dialog } -func (m *Model) Quitted() bool { return m.quitted } - -func (m *Model) DetectorResult() DetectorResult { return m.detectorResult } -func (m *Model) Spectrum() Spectrum { return *m.spectrum } +func (m *Model) Dialog() Dialog { return m.dialog } +func (m *Model) Quitted() bool { return m.quitted } // NewModelPlayer creates a new model and a player that communicates with it func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext, recoveryFilePath string) *Model { m := new(Model) m.synthers = synthers - m.MIDI = midiContext + m.midi = midiContext m.broker = broker m.d.Octave = 4 m.linkInstrTrack = true m.d.RecoveryFilePath = recoveryFilePath m.spectrum = broker.GetSpectrum() - m.resetSong() + m.Song().reset() if recoveryFilePath != "" { if bytes2, err := os.ReadFile(m.d.RecoveryFilePath); err == nil { var data modelData @@ -208,24 +189,60 @@ func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext } } TrySend(broker.ToPlayer, any(m.d.Song.Copy())) // we should be non-blocking in the constructor - m.signalAnalyzer = NewScopeModel(m.d.Song.BPM) + m.scopeData = scopeData{lengthInBeats: 4} + m.Scope().updateBufferLength() m.updateDeriveData(SongChange) - m.presets.load() - m.updateDerivedPresetSearch() + m.presetData.load() + m.Preset().updateCache() m.derived.searchResults = make([]string, 0, len(sointu.UnitNames)) - m.updateDerivedUnitSearch() + m.Unit().updateDerivedUnitSearch() + go runDetector(broker) + go runSpecAnalyzer(broker) return m } -func FindMIDIDeviceByPrefix(c MIDIContext, prefix string) (input string, ok bool) { - for input := range c.InputDevices { - if strings.HasPrefix(input, prefix) { - return input, true - } - } - return "", false +func (m *Model) Close() { + TrySend(m.broker.CloseDetector, struct{}{}) + TrySend(m.broker.CloseSpecAn, struct{}{}) + TimeoutReceive(m.broker.FinishedDetector, 3*time.Second) + TimeoutReceive(m.broker.FinishedSpecAn, 3*time.Second) } +// RequestQuit asks the tracker to quit, showing a dialog if there are unsaved +// changes. +func (m *Model) RequestQuit() Action { return MakeAction((*requestQuit)(m)) } + +type requestQuit Model + +func (m *requestQuit) Do() { + if !m.quitted { + m.dialog = QuitChanges + (*SongModel)(m).completeAction(true) + } +} + +// ForceQuit returns an Action to force the tracker to quit immediately, without +// saving any changes. +func (m *Model) ForceQuit() Action { return MakeAction((*forceQuit)(m)) } + +type forceQuit Model + +func (m *forceQuit) Do() { m.quitted = true } + +// ShowLicense returns an Action to show the software license dialog. +func (m *Model) ShowLicense() Action { return MakeAction((*showLicense)(m)) } + +type showLicense Model + +func (m *showLicense) Do() { m.dialog = License } + +// CancelDialog returns an Action to cancel the current dialog. +func (m *Model) CancelDialog() Action { return MakeAction((*cancelDialog)(m)) } + +type cancelDialog Model + +func (m *cancelDialog) Do() { m.dialog = NoDialog } + func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func() { if m.changeLevel == 0 { m.changeType = NoChange @@ -276,7 +293,7 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func( } if m.changeType&BPMChange != 0 { TrySend(m.broker.ToPlayer, any(BPMMsg{m.d.Song.BPM})) - m.signalAnalyzer.SetBpm(m.d.Song.BPM) + m.Scope().updateBufferLength() } if m.changeType&RowsPerBeatChange != 0 { TrySend(m.broker.ToPlayer, any(RowsPerBeatMsg{m.d.Song.RowsPerBeat})) @@ -306,65 +323,6 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func( } } -func (m *Model) MarshalRecovery() []byte { - out, err := json.Marshal(m.d) - if err != nil { - return nil - } - if m.d.RecoveryFilePath != "" { - os.Remove(m.d.RecoveryFilePath) - } - m.d.ChangedSinceRecovery = false - return out -} - -func (m *Model) SaveRecovery() error { - if !m.d.ChangedSinceRecovery { - return nil - } - if m.d.RecoveryFilePath == "" { - return errors.New("no backup file path") - } - out, err := json.Marshal(m.d) - if err != nil { - return fmt.Errorf("could not marshal recovery data: %w", err) - } - dir := filepath.Dir(m.d.RecoveryFilePath) - if _, err := os.Stat(dir); os.IsNotExist(err) { - os.MkdirAll(dir, os.ModePerm) - } - file, err := os.Create(m.d.RecoveryFilePath) - if err != nil { - return fmt.Errorf("could not create recovery file: %w", err) - } - _, err = file.Write(out) - if err != nil { - return fmt.Errorf("could not write recovery file: %w", err) - } - m.d.ChangedSinceRecovery = false - return nil -} - -func (m *Model) UnmarshalRecovery(bytes []byte) { - var data modelData - err := json.Unmarshal(bytes, &data) - if err != nil { - return - } - m.d = data - if m.d.RecoveryFilePath != "" { // check if there's a recovery file on disk and load it instead - if bytes2, err := os.ReadFile(m.d.RecoveryFilePath); err == nil { - var data modelData - if json.Unmarshal(bytes2, &data) == nil { - m.d = data - } - } - } - m.d.ChangedSinceRecovery = false - TrySend(m.broker.ToPlayer, any(m.d.Song.Copy())) - m.updateDeriveData(SongChange) -} - func (m *Model) ProcessMsg(msg MsgToModel) { if msg.HasPanicPlayerStatus { m.playerStatus = msg.PlayerStatus @@ -373,7 +331,7 @@ func (m *Model) ProcessMsg(msg MsgToModel) { m.d.Cursor2.SongPos = msg.PlayerStatus.SongPos TrySend(m.broker.ToGUI, any(MsgToGUI{ Kind: GUIMessageCenterOnRow, - Param: m.PlaySongRow(), + Param: m.Play().SongRow(), })) } m.panic = msg.Panic @@ -382,10 +340,10 @@ func (m *Model) ProcessMsg(msg MsgToModel) { m.detectorResult = msg.DetectorResult } if msg.TriggerChannel > 0 { - m.signalAnalyzer.Trigger(msg.TriggerChannel) + m.Scope().trigger(msg.TriggerChannel) } if msg.Reset { - m.signalAnalyzer.Reset() + m.Scope().reset() TrySend(m.broker.ToDetector, MsgToDetector{Reset: true}) // chain the messages: when the signal analyzer is reset, also reset the detector } switch e := msg.Data.(type) { @@ -402,13 +360,13 @@ func (m *Model) ProcessMsg(msg MsgToModel) { defer m.change("Recording", SongChange, MajorChange)() m.d.Song.Score = score m.d.Song.BPM = int(e.BPM + 0.5) - m.instrEnlarged = false + m.trackerHidden = false case Alert: m.Alerts().AddAlert(e) case IsPlayingMsg: m.playing = e.bool case *sointu.AudioBuffer: - m.signalAnalyzer.ProcessAudioBuffer(e) + m.Scope().processAudioBuffer(e) // chain the messages: when we have a new audio buffer, send them to the detector and the spectrum analyzer if m.specAnEnabled { // send buffers to spectrum analyzer only if it's enabled clone := m.broker.GetAudioBuffer() @@ -426,12 +384,7 @@ func (m *Model) ProcessMsg(msg MsgToModel) { } } -func (m *Model) CPULoad(buf []sointu.CPULoad) int { - return copy(buf, m.playerStatus.CPULoad[:m.playerStatus.NumThreads]) -} - -func (m *Model) SignalAnalyzer() *ScopeModel { return m.signalAnalyzer } -func (m *Model) Broker() *Broker { return m.broker } +func (m *Model) Broker() *Broker { return m.broker } func (d *modelData) Copy() modelData { ret := *d @@ -439,20 +392,6 @@ func (d *modelData) Copy() modelData { return ret } -func (m NullMIDIContext) InputDevices(yield func(string) bool) {} -func (m NullMIDIContext) Open(name string) error { return nil } -func (m NullMIDIContext) Close() {} -func (m NullMIDIContext) IsOpen() bool { return false } - -func (m *Model) resetSong() { - m.d.Song = defaultSong.Copy() - for _, instr := range m.d.Song.Patch { - (*Model)(m).assignUnitIDs(instr.Units) - } - m.d.FilePath = "" - m.d.ChangedSinceSave = false -} - func (m *Model) maxID() int { maxID := 0 for _, instr := range m.d.Song.Patch { diff --git a/tracker/model_test.go b/tracker/model_test.go index 281027c..7aac4af 100644 --- a/tracker/model_test.go +++ b/tracker/model_test.go @@ -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{}) diff --git a/tracker/note.go b/tracker/note.go new file mode 100644 index 0000000..c0e3587 --- /dev/null +++ b/tracker/note.go @@ -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} +} diff --git a/tracker/order.go b/tracker/order.go new file mode 100644 index 0000000..a26a051 --- /dev/null +++ b/tracker/order.go @@ -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 + } +} diff --git a/tracker/params.go b/tracker/params.go index 51bc4b9..53b68ae 100644 --- a/tracker/params.go +++ b/tracker/params.go @@ -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 } diff --git a/tracker/play.go b/tracker/play.go new file mode 100644 index 0000000..9087ca4 --- /dev/null +++ b/tracker/play.go @@ -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]) +} diff --git a/tracker/presets.go b/tracker/presets.go index 3cddc6f..26c91dc 100644 --- a/tracker/presets.go +++ b/tracker/presets.go @@ -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] } diff --git a/tracker/scope.go b/tracker/scope.go new file mode 100644 index 0000000..9cd90d4 --- /dev/null +++ b/tracker/scope.go @@ -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++ + } +} diff --git a/tracker/scopemodel.go b/tracker/scopemodel.go deleted file mode 100644 index ce37fba..0000000 --- a/tracker/scopemodel.go +++ /dev/null @@ -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) -} diff --git a/tracker/song.go b/tracker/song.go new file mode 100644 index 0000000..af5fe5f --- /dev/null +++ b/tracker/song.go @@ -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() + }() +} diff --git a/tracker/spectrum.go b/tracker/spectrum.go index 9973bd9..a34efa1 100644 --- a/tracker/spectrum.go +++ b/tracker/spectrum.go @@ -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]) } diff --git a/tracker/string.go b/tracker/string.go deleted file mode 100644 index fe5501b..0000000 --- a/tracker/string.go +++ /dev/null @@ -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 -} diff --git a/tracker/table.go b/tracker/table.go deleted file mode 100644 index 28e6335..0000000 --- a/tracker/table.go +++ /dev/null @@ -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} -} diff --git a/tracker/track.go b/tracker/track.go new file mode 100644 index 0000000..3b30d7b --- /dev/null +++ b/tracker/track.go @@ -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 +} diff --git a/tracker/unit.go b/tracker/unit.go new file mode 100644 index 0000000..6555b89 --- /dev/null +++ b/tracker/unit.go @@ -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 } diff --git a/tracker/voices.go b/tracker/voices.go new file mode 100644 index 0000000..f967a4b --- /dev/null +++ b/tracker/voices.go @@ -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 +}