From fa893c94f1c8c5cd6ebdac4b6d88ac5a6a311194 Mon Sep 17 00:00:00 2001 From: vsariola Date: Sun, 10 Jan 2021 01:51:16 +0200 Subject: [PATCH] feat(tracker): add simple instrument editor --- tracker/instruments.go | 121 +++++++++++++++++++++++++++++++++++++++++ tracker/layout.go | 2 + tracker/theme.go | 2 + tracker/tracker.go | 43 +++++++++------ 4 files changed, 152 insertions(+), 16 deletions(-) create mode 100644 tracker/instruments.go diff --git a/tracker/instruments.go b/tracker/instruments.go new file mode 100644 index 0000000..e1435ab --- /dev/null +++ b/tracker/instruments.go @@ -0,0 +1,121 @@ +package tracker + +import ( + "fmt" + "sort" + + "gioui.org/layout" + "gioui.org/widget" + "gioui.org/widget/material" +) + +type C = layout.Context +type D = layout.Dimensions + +func (t *Tracker) updateInstrumentScroll() { + if t.CurrentInstrument > 7 { + t.InstrumentList.Position.First = t.CurrentInstrument - 7 + } else { + t.InstrumentList.Position.First = 0 + } +} + +func (t *Tracker) layoutInstruments() layout.Widget { + return func(gtx C) D { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(t.layoutInstrumentNames()), + layout.Flexed(1, t.layoutInstrumentEditor())) + } +} + +func (t *Tracker) layoutInstrumentNames() layout.Widget { + return func(gtx C) D { + count := len(t.song.Patch.Instruments) + if len(t.InstrumentBtns) < count { + tail := make([]*widget.Clickable, count-len(t.InstrumentBtns)) + for t := range tail { + tail[t] = new(widget.Clickable) + } + t.InstrumentBtns = append(t.InstrumentBtns, tail...) + } + return t.InstrumentList.Layout(gtx, count, func(gtx C, index int) D { + for t.InstrumentBtns[index].Clicked() { + t.CurrentInstrument = index + } + btnStyle := material.Button(t.Theme, t.InstrumentBtns[index], fmt.Sprintf("%v", index)) + btnStyle.Background = transparent + return btnStyle.Layout(gtx) + }) + } +} +func (t *Tracker) layoutInstrumentEditor() layout.Widget { + return func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(t.layoutUnitList()), + layout.Rigid(t.layoutUnitControls())) + } +} + +func (t *Tracker) layoutUnitList() layout.Widget { + return func(gtx C) D { + units := t.song.Patch.Instruments[t.CurrentInstrument].Units + count := len(units) + if len(t.UnitBtns) < count { + tail := make([]*widget.Clickable, count-len(t.UnitBtns)) + for t := range tail { + tail[t] = new(widget.Clickable) + } + t.UnitBtns = append(t.UnitBtns, tail...) + } + children := make([]layout.FlexChild, len(t.song.Patch.Instruments[t.CurrentInstrument].Units)) + for i, u := range t.song.Patch.Instruments[t.CurrentInstrument].Units { + for t.UnitBtns[i].Clicked() { + t.CurrentUnit = i + } + btnStyle := material.Button(t.Theme, t.UnitBtns[i], u.Type) + btnStyle.Background = transparent + children[i] = layout.Rigid(btnStyle.Layout) + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) + } +} + +func (t *Tracker) layoutUnitControls() layout.Widget { + return func(gtx C) D { + params := t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Parameters + count := len(params) + children := make([]layout.FlexChild, 0, count) + if len(t.ParameterSliders) < count { + tail := make([]*widget.Float, count-len(t.ParameterSliders)) + for t := range tail { + tail[t] = new(widget.Float) + } + t.ParameterSliders = append(t.ParameterSliders, tail...) + } + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + for i, k := range keys { + for t.ParameterSliders[i].Changed() { + params[k] = int(t.ParameterSliders[i].Value) + // TODO: tracker should have functions to update parameters and + // to do this efficiently i.e. not compile the whole patch again + t.LoadSong(t.song) + } + t.ParameterSliders[i].Value = float32(params[k]) + sliderStyle := material.Slider(t.Theme, t.ParameterSliders[i], 0, 128) + k2 := k // avoid k changing in the closure + children = append(children, layout.Rigid(func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(Label(k2, white)), + layout.Rigid(func(gtx C) D { + gtx.Constraints.Min.X = 200 + return sliderStyle.Layout(gtx) + })) + })) + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) + } +} diff --git a/tracker/layout.go b/tracker/layout.go index ddb003b..857bf1e 100644 --- a/tracker/layout.go +++ b/tracker/layout.go @@ -59,6 +59,7 @@ func (t *Tracker) Layout(gtx layout.Context) { layout.Rigid(t.line(true, separatorLineColor)), layout.Flexed(1, t.layoutTracker)) }) + t.updateInstrumentScroll() } func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions { @@ -184,6 +185,7 @@ func (t *Tracker) layoutControls(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions { return in.Layout(gtx, enableButton(smallButton(material.IconButton(t.Theme, t.BPMDownBtn, downIcon)), t.song.BPM > 1).Layout) }), + layout.Flexed(1, t.layoutInstruments()), layout.Rigid(func(gtx layout.Context) layout.Dimensions { iconBtn := enableButton(material.IconButton(t.Theme, t.NewInstrumentBtn, addIcon), t.song.Patch.TotalVoices() < 32) return in.Layout(gtx, iconBtn.Layout) diff --git a/tracker/theme.go b/tracker/theme.go index 541ad74..0a51f29 100644 --- a/tracker/theme.go +++ b/tracker/theme.go @@ -22,6 +22,8 @@ var black = color.RGBA{R: 0, G: 0, B: 0, A: 255} var yellow = color.RGBA{R: 255, G: 255, B: 130, A: 255} var red = color.RGBA{R: 255, G: 0, B: 0, A: 255} +var transparent = color.RGBA{A: 0} + var primaryColorLight = color.RGBA{R: 243, G: 229, B: 245, A: 255} var primaryColor = color.RGBA{R: 206, G: 147, B: 216, A: 255} var primaryColorDark = color.RGBA{R: 123, G: 31, B: 162, A: 255} diff --git a/tracker/tracker.go b/tracker/tracker.go index 83ed7f1..3d1e360 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -5,6 +5,7 @@ import ( "sync" "gioui.org/font/gofont" + "gioui.org/layout" "gioui.org/widget" "gioui.org/widget/material" "github.com/vsariola/sointu" @@ -17,22 +18,28 @@ type Tracker struct { song sointu.Song Playing bool // protects PlayPattern and PlayRow - playRowPatMutex sync.RWMutex // protects song and playing - PlayPattern int - PlayRow int - CursorRow int - CursorColumn int - DisplayPattern int - ActiveTrack int - CurrentOctave byte - NoteTracking bool - Theme *material.Theme - OctaveUpBtn *widget.Clickable - OctaveDownBtn *widget.Clickable - BPMUpBtn *widget.Clickable - BPMDownBtn *widget.Clickable - NewTrackBtn *widget.Clickable - NewInstrumentBtn *widget.Clickable + playRowPatMutex sync.RWMutex // protects song and playing + PlayPattern int + PlayRow int + CursorRow int + CursorColumn int + DisplayPattern int + ActiveTrack int + CurrentInstrument int + CurrentUnit int + CurrentOctave byte + NoteTracking bool + Theme *material.Theme + OctaveUpBtn *widget.Clickable + OctaveDownBtn *widget.Clickable + BPMUpBtn *widget.Clickable + BPMDownBtn *widget.Clickable + NewTrackBtn *widget.Clickable + NewInstrumentBtn *widget.Clickable + ParameterSliders []*widget.Float + UnitBtns []*widget.Clickable + InstrumentBtns []*widget.Clickable + InstrumentList *layout.List sequencer *Sequencer ticked chan struct{} @@ -75,6 +82,9 @@ func (t *Tracker) LoadSong(song sointu.Song) error { if t.ActiveTrack >= len(song.Tracks) { t.ActiveTrack = len(song.Tracks) - 1 } + if t.sequencer != nil { + t.sequencer.SetSynth(t.synth) + } return nil } @@ -241,6 +251,7 @@ func New(audioContext sointu.AudioContext) *Tracker { closer: make(chan struct{}), undoStack: []sointu.Song{}, redoStack: []sointu.Song{}, + InstrumentList: &layout.List{Axis: layout.Horizontal}, } t.Theme.Color.Primary = primaryColor t.Theme.Color.InvText = black