diff --git a/patch.go b/patch.go index e4623a7..6ce37e6 100644 --- a/patch.go +++ b/patch.go @@ -153,7 +153,7 @@ var UnitTypes = map[string]([]UnitParameter){ "send": []UnitParameter{ {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, {Name: "amount", MinValue: 0, MaxValue: 128, CanSet: true, CanModulate: true, DisplayFunc: func(v int) (string, string) { return formatFloat(float64(v)/64 - 1), "" }}, - {Name: "voice", MinValue: 0, MaxValue: 32, CanSet: true, CanModulate: false}, + {Name: "voice", MinValue: 0, MaxValue: 32, CanSet: true, CanModulate: false, DisplayFunc: sendVoiceDispFunc}, {Name: "target", MinValue: 0, MaxValue: math.MaxInt32, CanSet: true, CanModulate: false}, {Name: "port", MinValue: 0, MaxValue: 7, CanSet: true, CanModulate: false}, {Name: "sendpop", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}}, @@ -246,6 +246,13 @@ func oscillatorTransposeDispFunc(v int) (string, string) { return strconv.Itoa(semitones), "st" } +func sendVoiceDispFunc(v int) (string, string) { + if v == 0 { + return "default", "" + } + return strconv.Itoa(v), "" +} + func engineeringTime(sec float64) (string, string) { if sec < 1e-3 { return fmt.Sprintf("%.2f", sec*1e6), "us" diff --git a/tracker/action.go b/tracker/action.go index f4b857c..a4144cf 100644 --- a/tracker/action.go +++ b/tracker/action.go @@ -308,8 +308,7 @@ func (m *Undo) Do() { m.d = m.undoStack[len(m.undoStack)-1] m.undoStack = m.undoStack[:len(m.undoStack)-1] m.prevUndoKind = "" - (*Model)(m).updateDerivedScoreData() - (*Model)(m).updateDerivedPatchData() + (*Model)(m).updateDeriveData(SongChange) TrySend(m.broker.ToPlayer, any(m.d.Song.Copy())) } @@ -326,8 +325,7 @@ func (m *Redo) Do() { m.d = m.redoStack[len(m.redoStack)-1] m.redoStack = m.redoStack[:len(m.redoStack)-1] m.prevUndoKind = "" - (*Model)(m).updateDerivedScoreData() - (*Model)(m).updateDerivedPatchData() + (*Model)(m).updateDeriveData(SongChange) TrySend(m.broker.ToPlayer, any(m.d.Song.Copy())) } diff --git a/tracker/derived.go b/tracker/derived.go index 16cf92a..35eaabd 100644 --- a/tracker/derived.go +++ b/tracker/derived.go @@ -2,176 +2,135 @@ package tracker import ( "fmt" - "iter" - "slices" "github.com/vsariola/sointu" ) type ( + Rail struct { + PassThrough int + Send bool + StackUse sointu.StackUse + } + + Wire struct { + From int + FromSet bool + To Point + ToSet bool + Hint string + } + + RailError struct { + InstrIndex, UnitIndex int + Err error + } + // derivedModelData contains useful information derived from the modelData, // cached for performance and/or easy access. This needs to be updated when // corresponding part of the model changes. derivedModelData struct { // map Unit by ID, other entities by their respective index - forUnit map[int]derivedForUnit - forTrack []derivedForTrack - forPattern []derivedForPattern - rail SignalRail // signal rail for the current patch + patch []derivedInstrument + tracks []derivedTrack + railError RailError } - derivedForUnit struct { - unit sointu.Unit - instrument sointu.Instrument - instrumentIndex int - unitIndex int - params []Parameter - - // map param by Name - forParameter map[string]derivedForParameter + derivedInstrument struct { + wires []Wire + rails []Rail + railsWidth int + params [][]Parameter + paramsWidth int } - derivedForParameter struct { - sendTooltip string - sendSources []sendSourceData - } - - sendSourceData struct { - unitId int - paramName string - amount int - instrumentIndex int - instrumentName string - } - - derivedForTrack struct { - instrumentRange []int - tracksWithSameInstrument []int - title string - } - - derivedForPattern struct { - useCount []int + derivedTrack struct { + title string + patternUseCounts []int } ) -// public access functions +// public methods to access the derived data -func (m *Model) UnitInfo(id int) (instrName string, units []sointu.Unit, unitIndex int, ok bool) { - fu, ok := m.derived.forUnit[id] - return fu.instrument.Name, fu.instrument.Units, fu.unitIndex, ok +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].railsWidth } -func (m *Model) UnitHintInfo(id int) (instrIndex int, unitType string, ok bool) { - fu, ok := m.derived.forUnit[id] - return fu.instrumentIndex, fu.unit.Type, ok -} - -func (m *Model) ParameterInfo(unitId int, paramName string) (tooltip string, ok bool) { - du, ok1 := m.derived.forUnit[unitId] - if !ok1 { - return "", false +func (m *Model) Wires(yield func(wire Wire) bool) { + i := m.d.InstrIndex + if i < 0 || i >= len(m.derived.patch) { + return } - dp, ok2 := du.forParameter[paramName] - if !ok2 { - return "", false + for _, wire := range m.derived.patch[i].wires { + if !yield(wire) { + return + } } - return dp.sendTooltip, len(dp.sendSources) > 0 } func (m *Model) TrackTitle(index int) string { - if index < 0 || index >= len(m.derived.forTrack) { + if index < 0 || index >= len(m.derived.tracks) { return "" } - return m.derived.forTrack[index].title + return m.derived.tracks[index].title } func (m *Model) PatternUnique(track, pat int) bool { - if track < 0 || track >= len(m.derived.forPattern) { + if track < 0 || track >= len(m.derived.tracks) { return false } - forPattern := m.derived.forPattern[track] - if pat < 0 || pat >= len(forPattern.useCount) { + if pat < 0 || pat >= len(m.derived.tracks[track].patternUseCounts) { return false } - return forPattern.useCount[pat] == 1 + return m.derived.tracks[track].patternUseCounts[pat] <= 1 } -// public getters with further model information +func (e *RailError) Error() string { return e.Err.Error() } -func (m *Model) TracksWithSameInstrumentAsCurrent() []int { - currentTrack := m.d.Cursor.Track - if currentTrack >= len(m.derived.forTrack) { - return nil - } - return m.derived.forTrack[currentTrack].tracksWithSameInstrument -} - -func (m *Model) CountNextTracksForCurrentInstrument() int { - currentTrack := m.d.Cursor.Track - count := 0 - for t := range m.TracksWithSameInstrumentAsCurrent() { - if t > currentTrack { - count++ - } - } - return count -} +func (s *Rail) StackAfter() int { return s.PassThrough + s.StackUse.NumOutputs } // init / update methods -func (m *Model) initDerivedData() { - m.derived = derivedModelData{ - forUnit: make(map[int]derivedForUnit), - forTrack: make([]derivedForTrack, 0), - forPattern: make([]derivedForPattern, 0), - } - m.updateDerivedScoreData() - m.updateDerivedPatchData() -} - -func (m *Model) updateDerivedScoreData() { - m.derived.forTrack = m.derived.forTrack[:0] - m.derived.forPattern = m.derived.forPattern[:0] - for index, track := range m.d.Song.Score.Tracks { - firstInstr, lastInstr, _ := m.instrumentRangeFor(index) - m.derived.forTrack = append( - m.derived.forTrack, - derivedForTrack{ - instrumentRange: []int{firstInstr, lastInstr}, - tracksWithSameInstrument: slices.Collect(m.tracksWithSameInstrument(index)), - title: m.buildTrackTitle(index), - }, - ) - m.derived.forPattern = append( - m.derived.forPattern, - derivedForPattern{ - useCount: m.calcPatternUseCounts(track), - }, - ) - } -} - -func (m *Model) updateDerivedPatchData() { - m.SignalRail().update() - clear(m.derived.forUnit) - for i, instr := range m.d.Song.Patch { - for u, unit := range instr.Units { - m.derived.forUnit[unit.ID] = derivedForUnit{ - unit: unit, - unitIndex: u, - instrument: instr, - instrumentIndex: i, - params: m.deriveParams(&instr.Units[u]), - forParameter: make(map[string]derivedForParameter), - } - m.updateDerivedParameterData(unit) +func (m *Model) updateDeriveData(changeType ChangeType) { + setSliceLength(&m.derived.tracks, len(m.d.Song.Score.Tracks)) + if changeType&ScoreChange != 0 { + for index, track := range m.d.Song.Score.Tracks { + m.derived.tracks[index].patternUseCounts = m.buildPatternUseCounts(track) } } + if changeType&ScoreChange != 0 || changeType&PatchChange != 0 { + for index := range m.d.Song.Score.Tracks { + m.derived.tracks[index].title = m.buildTrackTitle(index) + } + } + setSliceLength(&m.derived.patch, len(m.d.Song.Patch)) + if changeType&PatchChange != 0 { + m.updateParams() + m.updateRails() + m.updateWires() + } } -func (m *Model) deriveParams(unit *sointu.Unit) []Parameter { - ret := make([]Parameter, 0, 10) +func (m *Model) updateParams() { + for i, instr := range m.d.Song.Patch { + setSliceLength(&m.derived.patch[i].params, len(instr.Units)) + paramsWidth := 0 + for u, unit := range instr.Units { + m.derived.patch[i].params[u] = m.deriveParams(&unit, m.derived.patch[i].params[u]) + paramsWidth = max(paramsWidth, len(m.derived.patch[i].params[u])) + } + m.derived.patch[i].paramsWidth = paramsWidth + } +} + +func (m *Model) deriveParams(unit *sointu.Unit, ret []Parameter) []Parameter { + ret = ret[:0] // reset the slice unitType, ok := sointu.UnitTypes[unit.Type] if !ok { return ret @@ -211,73 +170,6 @@ func (m *Model) deriveParams(unit *sointu.Unit) []Parameter { return ret } -func (m *Model) updateDerivedParameterData(unit sointu.Unit) { - fu := m.derived.forUnit[unit.ID] - for name := range fu.unit.Parameters { - sendSources := slices.Collect(m.collectSendSources(unit, name)) - fu.forParameter[name] = derivedForParameter{ - sendSources: sendSources, - sendTooltip: m.buildSendTargetTooltip(fu.instrumentIndex, sendSources), - } - } -} - -// internals... - -func (m *Model) collectSendSources(unit sointu.Unit, paramName string) iter.Seq[sendSourceData] { - return func(yield func(sendSourceData) bool) { - for i, instr := range m.d.Song.Patch { - for _, u := range instr.Units { - if u.Type != "send" { - continue - } - targetId, ok := u.Parameters["target"] - if !ok || targetId != unit.ID { - continue - } - port := u.Parameters["port"] - unitParam, _, ok := sointu.FindParamForModulationPort(unit.Type, port) - if !ok || unitParam.Name != paramName { - continue - } - sourceData := sendSourceData{ - unitId: u.ID, - paramName: paramName, - instrumentIndex: i, - instrumentName: instr.Name, - amount: u.Parameters["amount"], - } - if !yield(sourceData) { - return - } - } - } - } -} - -func (m *Model) buildSendTargetTooltip(ownInstrIndex int, sendSources []sendSourceData) string { - if len(sendSources) == 0 { - return "" - } - amounts := "" - for _, sendSource := range sendSources { - sourceInfo := "" - if sendSource.instrumentIndex != ownInstrIndex { - sourceInfo = fmt.Sprintf(" from \"%s\"", sendSource.instrumentName) - } - if amounts == "" { - amounts = fmt.Sprintf("x %d%s", sendSource.amount, sourceInfo) - } else { - amounts = fmt.Sprintf("%s, x %d%s", amounts, sendSource.amount, sourceInfo) - } - } - count := "1 send" - if len(sendSources) > 1 { - count = fmt.Sprintf("%d sends", len(sendSources)) - } - return fmt.Sprintf("%s [%s]", count, amounts) -} - func (m *Model) instrumentRangeFor(trackIndex int) (int, int, error) { track := m.d.Song.Score.Tracks[trackIndex] if track.NumVoices <= 0 { @@ -296,86 +188,158 @@ func (m *Model) instrumentRangeFor(trackIndex int) (int, int, error) { return firstIndex, lastIndex, nil } -func (m *Model) buildTrackTitle(track int) (title string) { - title = "?" +func (m *Model) buildTrackTitle(track int) string { if track < 0 || track >= len(m.d.Song.Score.Tracks) { - return + return "?" } firstIndex, lastIndex, err := m.instrumentRangeFor(track) if err != nil { - return + return "?" } switch diff := lastIndex - firstIndex; diff { case 0: - title = m.d.Song.Patch[firstIndex].Name + return nilIsQuestionMark(m.d.Song.Patch[firstIndex].Name) + case 1: + return fmt.Sprintf("%s/%s", + nilIsQuestionMark(m.d.Song.Patch[firstIndex].Name), + nilIsQuestionMark(m.d.Song.Patch[firstIndex+1].Name)) default: - n1 := m.d.Song.Patch[firstIndex].Name - n2 := m.d.Song.Patch[firstIndex+1].Name - if len(n1) > 0 { - n1 = string(n1[0]) - } else { - n1 = "?" - } - if len(n2) > 0 { - n2 = string(n2[0]) - } else { - n2 = "?" - } - if diff > 1 { - title = n1 + "/" + n2 + "..." - } else { - title = n1 + "/" + n2 - } - } - return -} - -func (m *Model) instrumentForTrack(trackIndex int) (int, bool) { - voiceIndex := m.d.Song.Score.FirstVoiceForTrack(trackIndex) - instrument, err := m.d.Song.Patch.InstrumentForVoice(voiceIndex) - return instrument, err == nil -} - -func (m *Model) tracksWithSameInstrument(trackIndex int) iter.Seq[int] { - return func(yield func(int) bool) { - - currentInstrument, currentExists := m.instrumentForTrack(trackIndex) - if !currentExists { - return - } - - for i := 0; i < len(m.d.Song.Score.Tracks); i++ { - instrument, exists := m.instrumentForTrack(i) - if !exists { - return - } - if instrument != currentInstrument { - continue - } - if !yield(i) { - return - } - } + return fmt.Sprintf("%s/%s/...", + nilIsQuestionMark(m.d.Song.Patch[firstIndex].Name), + nilIsQuestionMark(m.d.Song.Patch[firstIndex+1].Name)) } } -func (m *Model) calcPatternUseCounts(track sointu.Track) []int { - result := make([]int, len(m.d.Song.Score.Tracks)) - for j := range result { - result[j] = 0 +func nilIsQuestionMark(s string) string { + if len(s) == 0 { + return "?" } - for j := 0; j < m.d.Song.Score.Length; j++ { - if j >= len(track.Order) { - break + return s +} + +func (m *Model) buildPatternUseCounts(track sointu.Track) []int { + result := make([]int, 0, len(track.Patterns)) + for j := range min(len(track.Order), m.d.Song.Score.Length) { + if p := track.Order[j]; p >= 0 { + for len(result) <= p { + result = append(result, 0) + } + result[p]++ } - p := track.Order[j] - for len(result) <= p { - result = append(result, 0) - } - if p < 0 { - continue - } - result[p]++ } return result } + +func (m *Model) updateRails() { + type stackElem struct{ instr, unit int } + scratchArray := [32]stackElem{} + scratch := scratchArray[:0] + m.derived.railError = RailError{} + for i, instr := range m.d.Song.Patch { + setSliceLength(&m.derived.patch[i].rails, len(instr.Units)) + start := len(scratch) + maxWidth := 0 + for u, unit := range instr.Units { + stackUse := unit.StackUse() + numInputs := len(stackUse.Inputs) + if len(scratch) < numInputs { + if m.derived.railError == (RailError{}) { + m.derived.railError = RailError{ + InstrIndex: i, + UnitIndex: u, + Err: fmt.Errorf("%s unit in instrument %d / %s needs %d inputs, but got only %d", unit.Type, i, instr.Name, numInputs, len(scratch)), + } + } + scratch = scratch[:0] + } else { + scratch = scratch[:len(scratch)-numInputs] + } + m.derived.patch[i].rails[u] = Rail{ + PassThrough: len(scratch), + StackUse: stackUse, + Send: unit.Type == "send", + } + maxWidth = max(maxWidth, len(scratch)+max(len(stackUse.Inputs), stackUse.NumOutputs)) + for range stackUse.NumOutputs { + scratch = append(scratch, stackElem{instr: i, unit: u}) + } + } + m.derived.patch[i].railsWidth = maxWidth + diff := len(scratch) - start + if instr.NumVoices > 1 && diff != 0 { + if diff < 0 { + morepop := (instr.NumVoices - 1) * diff + if morepop > len(scratch) { + if m.derived.railError == (RailError{}) { + m.derived.railError = RailError{ + InstrIndex: i, + UnitIndex: -1, + Err: fmt.Errorf("each voice of instrument %d / %s consumes %d signals, but there was not enough signals available", i, instr.Name, -diff), + } + } + scratch = scratch[:0] + } else { + scratch = scratch[:len(scratch)-morepop] + } + } else { + for range (instr.NumVoices - 1) * diff { + scratch = append(scratch, scratch[len(scratch)-diff]) + } + } + } + } + if len(scratch) > 0 && m.derived.railError == (RailError{}) { + patch := m.d.Song.Patch + m.derived.railError = RailError{ + InstrIndex: scratch[0].instr, + UnitIndex: scratch[0].unit, + Err: fmt.Errorf("instrument %d / %s unit %d / %s leave %d signals on stack, but no more signals available", scratch[0].instr, patch[scratch[0].instr].Name, scratch[0].unit, patch[scratch[0].instr].Units[scratch[0].unit].Type, len(scratch)), + } + } + if m.derived.railError.Err != nil { + m.Alerts().AddNamed("RailError", m.derived.railError.Error(), Error) + } +} + +func (m *Model) updateWires() { + for i := range m.d.Song.Patch { + m.derived.patch[i].wires = m.derived.patch[i].wires[:0] // reset the wires + } + for i, instr := range m.d.Song.Patch { + for u, unit := range instr.Units { + if unit.Type != "send" { + continue + } + tI, tU, err := m.d.Song.Patch.FindUnit(unit.Parameters["target"]) + if err != nil { + continue + } + _, tX, ok := sointu.FindParamForModulationPort(m.d.Song.Patch[tI].Units[tU].Type, unit.Parameters["port"]) + if !ok { + continue + } + if tI == i { + // local send + m.derived.patch[i].wires = append(m.derived.patch[i].wires, Wire{ + From: u, + FromSet: true, + To: Point{X: tX, Y: tU}, + ToSet: true, + Hint: "TBW", + }) + } else { + // remote send + m.derived.patch[i].wires = append(m.derived.patch[i].wires, Wire{ + From: u, + FromSet: true, + Hint: "TBW", + }) + m.derived.patch[tI].wires = append(m.derived.patch[tI].wires, Wire{ + To: Point{X: tX, Y: tU}, + ToSet: true, + Hint: "TBW", + }) + } + } + } +} diff --git a/tracker/gioui/patch_panel.go b/tracker/gioui/patch_panel.go index 34fb634..c087111 100644 --- a/tracker/gioui/patch_panel.go +++ b/tracker/gioui/patch_panel.go @@ -2,7 +2,6 @@ package gioui import ( "bytes" - "fmt" "image" "image/color" "io" @@ -371,79 +370,45 @@ func MakeUnitList(m *tracker.Model) UnitList { func (ul *UnitList) Layout(gtx C) D { t := TrackerFromContext(gtx) ul.update(gtx, t) - var units [256]tracker.UnitListItem - for i, item := range (*tracker.Units)(t.Model).Iterate { - if i >= 256 { - break - } - units[i] = item - } - count := min(ul.dragList.TrackerList.Count(), 256) element := func(gtx C, i int) D { gtx.Constraints.Max.Y = gtx.Dp(20) gtx.Constraints.Min.Y = gtx.Constraints.Max.Y - if i < 0 || i > 255 { - return layout.Dimensions{Size: gtx.Constraints.Min} - } - u := units[i] + u := t.Units().Item(i) editorStyle := t.Theme.InstrumentEditor.UnitList.Name - if u.Disabled { + signalError := t.RailError() + switch { + case u.Disabled: editorStyle = t.Theme.InstrumentEditor.UnitList.NameDisabled - } - stackText := strconv.FormatInt(int64(u.StackAfter), 10) - if u.StackNeed > u.StackBefore { + case signalError.Err != nil && signalError.UnitIndex == i: editorStyle.Color = t.Theme.InstrumentEditor.UnitList.Error - (*tracker.Alerts)(t.Model).AddNamed("UnitNeedsInputs", fmt.Sprintf("%v needs at least %v input signals, got %v", u.Type, u.StackNeed, u.StackBefore), tracker.Error) - } else if i == count-1 && u.StackAfter != 0 { - editorStyle.Color = t.Theme.InstrumentEditor.UnitList.Warning - (*tracker.Alerts)(t.Model).AddNamed("InstrumentLeavesSignals", fmt.Sprintf("Instrument leaves %v signal(s) on the stack", u.StackAfter), tracker.Warning) } - stackLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Stack, stackText) - rightMargin := layout.Inset{Right: unit.Dp(10)} - return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Rigid(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() - str := t.Model.UnitSearch() - 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.Units().SetSelectedType(n) - break - } - } - } else { - t.Units().SetSelectedType("") - } - } - ul.dragList.Focus() - t.UnitSearching().SetValue(false) - } - return ul.searchEditor.Layout(gtx, str, t.Theme, &editorStyle, "---") - } else { - text := u.Type - if text == "" { - text = "---" - } - l := editorStyle.AsLabelStyle() - return Label(t.Theme, &l, text).Layout(gtx) + 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, "---") + } else { + text := u.Type + if text == "" { + text = "---" } - }), - layout.Flexed(1, func(gtx C) D { - unitNameLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Comment, u.Comment) - inset := layout.Inset{Left: unit.Dp(5)} - return inset.Layout(gtx, unitNameLabel.Layout) - }), - layout.Rigid(func(gtx C) D { - return rightMargin.Layout(gtx, stackLabel.Layout) - }), + l := editorStyle.AsLabelStyle() + return Label(t.Theme, &l, text).Layout(gtx) + } + } + stackText := strconv.FormatInt(int64(u.Signals.StackAfter()), 10) + commentLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Comment, u.Comment) + stackLabel := Label(t.Theme, &t.Theme.InstrumentEditor.UnitList.Stack, stackText) + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(unitName), + layout.Rigid(layout.Spacer{Width: 5}.Layout), + layout.Flexed(1, commentLabel.Layout), + layout.Rigid(stackLabel.Layout), + layout.Rigid(layout.Spacer{Width: 10}.Layout), ) } defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() unitList := FilledDragList(t.Theme, ul.dragList) - return Surface{Gray: 30, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, func(gtx C) D { + surface := func(gtx C) D { return layout.Stack{Alignment: layout.SE}.Layout(gtx, layout.Expanded(func(gtx C) D { defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() @@ -458,7 +423,8 @@ func (ul *UnitList) Layout(gtx C) D { return margin.Layout(gtx, addUnitBtn.Layout) }), ) - }) + } + return Surface{Gray: 30, Focus: t.PatchPanel.TreeFocused(gtx)}.Layout(gtx, surface) } func (ul *UnitList) update(gtx C, t *Tracker) { @@ -490,6 +456,23 @@ func (ul *UnitList) update(gtx C, t *Tracker) { } } } + str := t.Model.UnitSearch() + 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.Units().SetSelectedType(n) + break + } + } + } else { + t.Units().SetSelectedType("") + } + } + ul.dragList.Focus() + t.UnitSearching().SetValue(false) + } } func (ul *UnitList) Tags(curLevel int, yield TagYieldFunc) bool { diff --git a/tracker/gioui/signal_rail.go b/tracker/gioui/signal_rail.go index 8aa4073..d5fc88b 100644 --- a/tracker/gioui/signal_rail.go +++ b/tracker/gioui/signal_rail.go @@ -25,12 +25,12 @@ type ( SignalRailWidget struct { Style *SignalRailStyle - Signal tracker.Signal + Signal tracker.Rail Height unit.Dp } ) -func SignalRail(th *Theme, signal tracker.Signal) SignalRailWidget { +func SignalRail(th *Theme, signal tracker.Rail) SignalRailWidget { return SignalRailWidget{ Style: &th.SignalRail, Signal: signal, @@ -41,6 +41,9 @@ func SignalRail(th *Theme, signal tracker.Signal) SignalRailWidget { func (s SignalRailWidget) Layout(gtx C) D { sw := gtx.Dp(s.Style.SignalWidth) h := gtx.Dp(s.Height) + if s.Signal.PassThrough == 0 && len(s.Signal.StackUse.Inputs) == 0 && s.Signal.StackUse.NumOutputs == 0 { + return D{Size: image.Pt(sw, h)} + } lw := gtx.Dp(s.Style.LineWidth) pd := gtx.Dp(s.Style.PortDiameter) center := sw / 2 diff --git a/tracker/gioui/theme.go b/tracker/gioui/theme.go index 1d9f70e..630a2db 100644 --- a/tracker/gioui/theme.go +++ b/tracker/gioui/theme.go @@ -85,14 +85,14 @@ type Theme struct { } } UnitEditor struct { - Name LabelStyle - Chooser LabelStyle - Hint LabelStyle - InvalidParam color.NRGBA - SendTarget color.NRGBA - Width unit.Dp - Height unit.Dp - UnitList struct { + Name LabelStyle + Chooser LabelStyle + Hint LabelStyle + WireColor color.NRGBA + WireHint LabelStyle + Width unit.Dp + Height unit.Dp + UnitList struct { LabelWidth unit.Dp Name LabelStyle Disabled LabelStyle diff --git a/tracker/gioui/theme.yml b/tracker/gioui/theme.yml index ad2659e..fc77cd4 100644 --- a/tracker/gioui/theme.yml +++ b/tracker/gioui/theme.yml @@ -210,8 +210,8 @@ uniteditor: chooser: { textsize: 12, color: *white, shadowcolor: *black } name: { textsize: 12, alignment: 2, color: *highemphasis, shadowcolor: *black } - invalidparam: { r: 120, g: 120, b: 120, a: 190 } - sendtarget: *secondarycolor + wirecolor: *secondarycolor + wirehint: { textsize: 12, color: *disabled, shadowcolor: *black } width: 60 height: 70 unitlist: diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index d055fb8..1811f52 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -162,9 +162,9 @@ func (pe *UnitEditor) 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.SignalRail().MaxWidth() + rowTitleSignalWidth := gtx.Dp(t.Theme.SignalRail.SignalWidth) * t.RailWidth() rowTitleWidth := rowTitleLabelWidth + rowTitleSignalWidth - signalError := t.SignalRail().Error() + signalError := t.RailError() columnTitleHeight := gtx.Dp(0) for i := range pe.Parameters { for len(pe.Parameters[i]) < width { @@ -182,11 +182,11 @@ func (pe *UnitEditor) layoutRack(gtx C) D { if y < 0 || y >= len(pe.Parameters) { return D{} } - - sr := SignalRail(t.Theme, t.SignalRail().Item(y)) - label := Label(t.Theme, &t.Theme.UnitEditor.UnitList.Name, t.Units().Item(y).Type) + item := t.Units().Item(y) + sr := SignalRail(t.Theme, item.Signals) + label := Label(t.Theme, &t.Theme.UnitEditor.UnitList.Name, item.Type) switch { - case t.Units().Item(y).Disabled: + case item.Disabled: label.LabelStyle = t.Theme.UnitEditor.UnitList.Disabled case signalError.Err != nil && signalError.UnitIndex == y: label.Color = t.Theme.UnitEditor.UnitList.Error @@ -230,7 +230,6 @@ func (pe *UnitEditor) layoutRack(gtx C) D { func (pe *UnitEditor) drawSignals(gtx C, rowTitleWidth int) { t := TrackerFromContext(gtx) - units := t.Units() colP := pe.paramTable.ColTitleList.List.Position rowP := pe.paramTable.RowTitleList.List.Position p := image.Pt(rowTitleWidth, 0) @@ -238,16 +237,25 @@ func (pe *UnitEditor) 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 i := 0; i < units.Count(); i++ { - item := units.Item(i) - if item.TargetOk { - pe.drawSignal(gtx, i-rowP.First, item.TargetX-colP.First, item.TargetY-rowP.First) - } + for wire := range t.Wires { + pe.drawSignal(gtx, wire, colP.First, rowP.First) } } -func (pe *UnitEditor) drawSignal(gtx C, sy, ex, ey int) { +func (pe *UnitEditor) drawSignal(gtx C, wire tracker.Wire, col, row int) { + sy := wire.From - row + ex := wire.To.X - col + ey := wire.To.Y - row t := TrackerFromContext(gtx) + if wire.FromSet && !wire.ToSet { + defer op.Offset(image.Pt(0, (sy+1)*gtx.Dp(t.Theme.UnitEditor.Height)-gtx.Dp(16))).Push(gtx.Ops).Pop() + Label(t.Theme, &t.Theme.UnitEditor.WireHint, wire.Hint).Layout(gtx) + return + } + if !wire.FromSet && wire.ToSet { + Label(t.Theme, &t.Theme.UnitEditor.WireHint, wire.Hint).Layout(gtx) + return + } width := float32(gtx.Dp(t.Theme.UnitEditor.Width)) height := float32(gtx.Dp(t.Theme.UnitEditor.Height)) diam := gtx.Dp(t.Theme.Knob.Diameter) @@ -278,7 +286,7 @@ func (pe *UnitEditor) drawSignal(gtx C, sy, ex, ey int) { path.MoveTo(from) path.CubeTo(from.Add(fromTan), p1.Sub(p1Tan), p1) path.CubeTo(p1.Add(p1Tan), p2, to) - paint.FillShape(gtx.Ops, t.Theme.UnitEditor.SendTarget, + paint.FillShape(gtx.Ops, t.Theme.UnitEditor.WireColor, clip.Stroke{ Path: path.End(), Width: float32(gtx.Dp(t.Theme.SignalRail.LineWidth)), diff --git a/tracker/list.go b/tracker/list.go index 2c9405b..ac7314c 100644 --- a/tracker/list.go +++ b/tracker/list.go @@ -36,11 +36,9 @@ type ( } UnitListItem struct { - Type, Comment string - Disabled bool - StackNeed, StackBefore, StackAfter int - TargetOk bool // TargetOk indicates if the target unit is valid - TargetX, TargetY int + Type, Comment string + Disabled bool + Signals Rail } // Range is used to represent a range [Start,End) of integers @@ -301,55 +299,21 @@ func (m *Units) SetSelectedType(t string) { m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].ID = oldUnit.ID // keep the ID of the replaced unit } -func (v *Units) Iterate(yield UnitYieldFunc) { - if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) { - return - } - stackBefore := 0 - for i, unit := range v.d.Song.Patch[v.d.InstrIndex].Units { - stackAfter := stackBefore + unit.StackChange() - if !yield(i, UnitListItem{ - Type: unit.Type, - Comment: unit.Comment, - Disabled: unit.Disabled, - StackNeed: unit.StackNeed(), - StackBefore: stackBefore, - StackAfter: stackAfter, - }) { - break - } - stackBefore = stackAfter - } -} - func (v *Units) Item(index int) UnitListItem { - if index < 0 || index >= v.Count() { + i := v.d.InstrIndex + if i < 0 || i >= len(v.d.Song.Patch) || index < 0 || index >= v.Count() { return UnitListItem{} } unit := v.d.Song.Patch[v.d.InstrIndex].Units[index] - targetOk := false - targetY := 0 - targetX := 0 - if unit.Type == "send" { - if i, y, err := v.d.Song.Patch.FindUnit(unit.Parameters["target"]); err == nil { - targetUnit := v.d.Song.Patch[i].Units[y] - if _, x, ok := sointu.FindParamForModulationPort(targetUnit.Type, unit.Parameters["port"]); ok { - targetX = x - targetY = y - targetOk = true - } - } + 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, - StackNeed: unit.StackNeed(), - TargetOk: targetOk, - TargetY: targetY, - TargetX: targetX, - StackBefore: 0, - StackAfter: 0, + Type: unit.Type, + Comment: unit.Comment, + Disabled: unit.Disabled, + Signals: signals, } } diff --git a/tracker/model.go b/tracker/model.go index 42e8089..f012ff5 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -190,7 +190,7 @@ func NewModel(broker *Broker, synther sointu.Synther, midiContext MIDIContext, r } TrySend(broker.ToPlayer, any(m.d.Song.Copy())) // we should be non-blocking in the constructor m.signalAnalyzer = NewScopeModel(broker, m.d.Song.BPM) - m.initDerivedData() + m.updateDeriveData(SongChange) return m } @@ -224,7 +224,6 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func( if m.changeType&ScoreChange != 0 { m.d.Cursor.SongPos = m.d.Song.Score.Clamp(m.d.Cursor.SongPos) m.d.Cursor2.SongPos = m.d.Song.Score.Clamp(m.d.Cursor2.SongPos) - m.updateDerivedScoreData() TrySend(m.broker.ToPlayer, any(m.d.Song.Score.Copy())) } if m.changeType&PatchChange != 0 { @@ -241,7 +240,6 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func( m.d.UnitSearching = false // if we change anything in the patch, reset the unit searching m.d.UnitSearchString = "" m.d.SendSource = 0 - m.updateDerivedPatchData() TrySend(m.broker.ToPlayer, any(m.d.Song.Patch.Copy())) } if m.changeType&BPMChange != 0 { @@ -251,6 +249,7 @@ func (m *Model) change(kind string, t ChangeType, severity ChangeSeverity) func( if m.changeType&RowsPerBeatChange != 0 { TrySend(m.broker.ToPlayer, any(RowsPerBeatMsg{m.d.Song.RowsPerBeat})) } + m.updateDeriveData(m.changeType) m.undoSkipCounter++ var limit int switch m.changeSeverity { @@ -331,7 +330,7 @@ func (m *Model) UnmarshalRecovery(bytes []byte) { } m.d.ChangedSinceRecovery = false TrySend(m.broker.ToPlayer, any(m.d.Song.Copy())) - m.initDerivedData() + m.updateDeriveData(SongChange) } func (m *Model) ProcessMsg(msg MsgToModel) { diff --git a/tracker/params.go b/tracker/params.go index eff7ca8..52107ba 100644 --- a/tracker/params.go +++ b/tracker/params.go @@ -30,7 +30,6 @@ type ( Type(*Parameter) ParameterType Name(*Parameter) string Hint(*Parameter) ParameterHint - Info(*Parameter) (string, bool) // additional info for the parameter, used to display send targets LargeStep(*Parameter) int Reset(*Parameter) } @@ -112,12 +111,6 @@ func (p *Parameter) Hint() ParameterHint { } return p.vtable.Hint(p) } -func (p *Parameter) Info() (string, bool) { - if p.vtable == nil { - return "", false - } - return p.vtable.Info(p) -} func (p *Parameter) LargeStep() int { if p.vtable == nil { return 1 @@ -160,14 +153,10 @@ func (pt *Params) SetCursor(p Point) { } func (pt *Params) SetCursor2(p Point) {} func (pt *Params) Width() int { - if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.d.Song.Patch) { + if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.derived.patch) { return 0 } - ret := 0 - for _, unit := range pt.d.Song.Patch[pt.d.InstrIndex].Units { - ret = max(ret, len(pt.derived.forUnit[unit.ID].params)) - } - return ret + return pt.derived.patch[pt.d.InstrIndex].paramsWidth } func (pt *Params) Height() int { return (*Model)(pt).Units().Count() } func (pt *Params) MoveCursor(dx, dy int) (ok bool) { @@ -178,15 +167,10 @@ func (pt *Params) MoveCursor(dx, dy int) (ok bool) { return p == pt.Cursor() } func (pt *Params) Item(p Point) Parameter { - if pt.d.InstrIndex < 0 || pt.d.InstrIndex >= len(pt.d.Song.Patch) || - p.Y < 0 || p.Y >= len(pt.d.Song.Patch[pt.d.InstrIndex].Units) { + 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{} } - id := pt.d.Song.Patch[pt.d.InstrIndex].Units[p.Y].ID - if p.X < 0 || p.X >= len(pt.derived.forUnit[id].params) { - return Parameter{} - } - return pt.derived.forUnit[id].params[p.X] + return pt.derived.patch[pt.d.InstrIndex].params[p.Y][p.X] } func (pt *Params) clear(p Point) { panic("NOT IMPLEMENTED") @@ -246,32 +230,8 @@ func (n *namedParameter) Hint(p *Parameter) ParameterHint { valueInUnits, units := p.up.DisplayFunc(val) label = fmt.Sprintf("%s %s", valueInUnits, units) } - if p.unit.Type == "send" { - instrIndex, targetType, ok := p.m.UnitHintInfo(p.unit.Parameters["target"]) - if p.up.Name == "voice" && val == 0 { - if ok && instrIndex != p.m.d.InstrIndex { - label = "all" - } else { - label = "self" - } - } - if p.up.Name == "port" { - if !ok { - return ParameterHint{label, false} - } - portList := sointu.Ports[targetType] - if val < 0 || val >= len(portList) { - return ParameterHint{label, false} - } - label = portList[val] - } - } return ParameterHint{label, true} } -func (n *namedParameter) Info(p *Parameter) (string, bool) { - sendInfo, ok := p.m.ParameterInfo(p.unit.ID, p.up.Name) - return sendInfo, ok -} func (n *namedParameter) LargeStep(p *Parameter) int { if p.up.Name == "transpose" { return 12 @@ -328,9 +288,6 @@ func (g *gmDlsEntryParameter) Hint(p *Parameter) ParameterHint { } return ParameterHint{label, true} } -func (g *gmDlsEntryParameter) Info(p *Parameter) (string, bool) { - return "", false -} func (g *gmDlsEntryParameter) LargeStep(p *Parameter) int { return 16 } @@ -404,9 +361,6 @@ func (d *delayTimeParameter) Hint(p *Parameter) ParameterHint { } return ParameterHint{text, true} } -func (d *delayTimeParameter) Info(p *Parameter) (string, bool) { - return "", false -} func (d *delayTimeParameter) LargeStep(p *Parameter) int { return 16 } @@ -445,9 +399,6 @@ func (d *delayLinesParameter) Name(p *Parameter) string { func (d *delayLinesParameter) Hint(p *Parameter) ParameterHint { return ParameterHint{strconv.Itoa(d.Value(p)), true} } -func (d *delayLinesParameter) Info(p *Parameter) (string, bool) { - return "", false -} func (d *delayLinesParameter) LargeStep(p *Parameter) int { return 4 } @@ -490,9 +441,6 @@ func (r *reverbParameter) Hint(p *Parameter) ParameterHint { } return ParameterHint{label, true} } -func (r *reverbParameter) Info(p *Parameter) (string, bool) { - return "", false -} func (r *reverbParameter) LargeStep(p *Parameter) int { return 1 } diff --git a/tracker/stack.go b/tracker/stack.go deleted file mode 100644 index 82e8699..0000000 --- a/tracker/stack.go +++ /dev/null @@ -1,138 +0,0 @@ -package tracker - -import ( - "fmt" - - "github.com/vsariola/sointu" -) - -type ( - SignalRail struct { - signals [][]Signal - scratch []signalScratch - - error SignalError - } - - SignalError struct { - InstrIndex, UnitIndex int - Err error - } - - Signal struct { - PassThrough int - Send bool - StackUse sointu.StackUse - } - - signalScratch struct { - instr, unit int - } - - SignalRailType Model -) - -func (m *Model) SignalRail() *SignalRailType { - return (*SignalRailType)(m) -} - -func (s *SignalRailType) Item(u int) Signal { - i := s.d.InstrIndex - if i < 0 || u < 0 || i >= len(s.derived.rail.signals) || u >= len(s.derived.rail.signals[i]) { - return Signal{} - } - return s.derived.rail.signals[i][u] -} - -func (s *SignalRailType) Error() SignalError { - i := s.d.InstrIndex - if i < 0 || i >= len(s.derived.rail.signals) { - return SignalError{} - } - if i == s.derived.rail.error.InstrIndex { - return s.derived.rail.error - } - return SignalError{} -} - -func (s *SignalRailType) MaxWidth() int { - i := s.d.InstrIndex - if i < 0 || i >= len(s.derived.rail.signals) { - return 0 - } - ret := 0 - for _, signal := range s.derived.rail.signals[i] { - ret = max(ret, signal.PassThrough+max(len(signal.StackUse.Inputs), signal.StackUse.NumOutputs)) - } - return ret -} - -func (st *SignalRailType) update() { - s := &st.derived.rail - patch := st.d.Song.Patch - s.scratch = s.scratch[:0] - s.error = SignalError{} - for i, instr := range patch { - for len(s.signals) <= i { - s.signals = append(s.signals, make([]Signal, len(instr.Units))) - } - start := len(s.scratch) - for u, unit := range instr.Units { - for len(s.signals[i]) <= u { - s.signals[i] = append(s.signals[i], Signal{}) - } - stackUse := unit.StackUse() - numInputs := len(stackUse.Inputs) - if len(s.scratch) < numInputs { - if s.error.Err == nil { - s.error.Err = fmt.Errorf("%s unit in instrument %d / %s needs %d inputs, but got only %d", unit.Type, i, instr.Name, numInputs, len(s.scratch)) - s.error.InstrIndex = i - s.error.UnitIndex = u - } - s.scratch = s.scratch[:0] - } else { - s.scratch = s.scratch[:len(s.scratch)-numInputs] - } - s.signals[i][u] = Signal{ - PassThrough: len(s.scratch), - StackUse: stackUse, - Send: unit.Type == "send", - } - for _ = range stackUse.NumOutputs { - s.scratch = append(s.scratch, signalScratch{instr: i, unit: u}) - } - } - diff := len(s.scratch) - start - if instr.NumVoices > 1 && diff != 0 { - if diff < 0 { - morepop := (instr.NumVoices - 1) * diff - if morepop > len(s.scratch) { - if s.error.Err == nil { - s.error.Err = fmt.Errorf("each voice of instrument %d / %s consumes %d signals, but there was not enough signals available", i, instr.Name, -diff) - s.error.InstrIndex = i - s.error.UnitIndex = -1 - } - s.scratch = s.scratch[:0] - } else { - s.scratch = s.scratch[:len(s.scratch)-morepop] - } - } else { - for range (instr.NumVoices - 1) * diff { - s.scratch = append(s.scratch, s.scratch[len(s.scratch)-diff]) - } - } - } - } - if len(s.scratch) > 0 && s.error.Err == nil { - s.error.Err = fmt.Errorf("instrument %d / %s unit %d / %s leave a signal on stack ", s.scratch[0].instr, patch[s.scratch[0].instr].Name, s.scratch[0].unit, patch[s.scratch[0].instr].Units[s.scratch[0].unit].Type) - s.error.InstrIndex = s.scratch[0].instr - s.error.UnitIndex = s.scratch[0].unit - } - if s.error.Err != nil { - (*Model)(st).Alerts().AddNamed("SignalError", s.error.Error(), Error) - } -} - -func (e *SignalError) Error() string { - return e.Err.Error() -}