From cbf9d3473859e865da6efd80cde2aec63a1b054f Mon Sep 17 00:00:00 2001 From: vsariola Date: Fri, 8 Jan 2021 18:55:02 +0200 Subject: [PATCH] feat(tracker): add new instrument & new track buttons --- sointu.go | 8 +++ tracker/defaultsong.go | 10 ++++ tracker/layout.go | 69 ++++++++++++++++++++------ tracker/patterns.go | 2 +- tracker/sequencer.go | 2 + tracker/theme.go | 4 +- tracker/tracker.go | 108 ++++++++++++++++++++++++++++------------- 7 files changed, 151 insertions(+), 52 deletions(-) diff --git a/sointu.go b/sointu.go index ed2c702..b253dad 100644 --- a/sointu.go +++ b/sointu.go @@ -241,6 +241,14 @@ func (s *Song) FirstTrackVoice(track int) int { return ret } +func (s *Song) TotalTrackVoices() int { + ret := 0 + for _, t := range s.Tracks { + ret += t.NumVoices + } + return ret +} + // TBD: Where shall we put methods that work on pure domain types and have no dependencies // e.g. Validate here func (s *Song) Validate() error { diff --git a/tracker/defaultsong.go b/tracker/defaultsong.go index f84a08b..5138071 100644 --- a/tracker/defaultsong.go +++ b/tracker/defaultsong.go @@ -2,6 +2,16 @@ package tracker import "github.com/vsariola/sointu" +var defaultInstrument = sointu.Instrument{ + NumVoices: 1, + Units: []sointu.Unit{ + {Type: "envelope", Parameters: map[string]int{"stereo": 1, "attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 64}}, + {Type: "oscillator", Parameters: map[string]int{"stereo": 1, "transpose": 64, "detune": 64, "phase": 0, "color": 128, "shape": 64, "gain": 64, "type": sointu.Sine}}, + {Type: "mulp", Parameters: map[string]int{"stereo": 1}}, + {Type: "out", Parameters: map[string]int{"stereo": 1, "gain": 64}}, + }, +} + var defaultSong = sointu.Song{ BPM: 100, Tracks: []sointu.Track{ diff --git a/tracker/layout.go b/tracker/layout.go index 0952f71..fe7cd87 100644 --- a/tracker/layout.go +++ b/tracker/layout.go @@ -18,6 +18,7 @@ import ( var upIcon *widget.Icon var downIcon *widget.Icon +var addIcon *widget.Icon func init() { var err error @@ -29,6 +30,10 @@ func init() { if err != nil { log.Fatal(err) } + addIcon, err = widget.NewIcon(icons.ContentAdd) + if err != nil { + log.Fatal(err) + } } func (t *Tracker) Layout(gtx layout.Context) { @@ -42,7 +47,7 @@ func (t *Tracker) Layout(gtx layout.Context) { } func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions { - flexTracks := make([]layout.FlexChild, len(t.song.Tracks)+1) + flexTracks := make([]layout.FlexChild, len(t.song.Tracks)) t.playRowPatMutex.RLock() defer t.playRowPatMutex.RUnlock() @@ -51,7 +56,7 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions { playPat = -1 } - flexTracks[0] = layout.Rigid(Lowered(t.layoutRowMarkers( + rowMarkers := layout.Rigid(Lowered(t.layoutRowMarkers( len(t.song.Tracks[0].Patterns[0]), len(t.song.Tracks[0].Sequence), t.CursorRow, @@ -61,7 +66,7 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions { playPat, ))) for i, trk := range t.song.Tracks { - flexTracks[i+1] = layout.Rigid(Lowered(t.layoutTrack( + flexTracks[i] = layout.Rigid(Lowered(t.layoutTrack( trk.Patterns, trk.Sequence, t.ActiveTrack == i, @@ -72,8 +77,31 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions { playPat, ))) } + in := layout.UniformInset(unit.Dp(8)) + buttons := layout.Rigid(func(gtx layout.Context) layout.Dimensions { + iconBtn := material.IconButton(t.Theme, t.NewTrackBtn, addIcon) + if t.song.TotalTrackVoices() >= t.song.Patch.TotalVoices() { + iconBtn.Color = inactiveBtnColor + } + return in.Layout(gtx, iconBtn.Layout) + }) + go func() { + for t.NewTrackBtn.Clicked() { + t.AddTrack() + } + }() return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - flexTracks..., + rowMarkers, + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + defer op.Push(gtx.Ops).Pop() + clip.Rect{Max: gtx.Constraints.Max}.Add(gtx.Ops) + dims := layout.Flex{Axis: layout.Horizontal}.Layout(gtx, flexTracks...) + if dims.Size.X > gtx.Constraints.Max.X { + dims.Size.X = gtx.Constraints.Max.X + } + return dims + }), + buttons, ) } @@ -87,18 +115,23 @@ func (t *Tracker) layoutControls(gtx layout.Context) layout.Dimensions { } in := layout.UniformInset(unit.Dp(8)) - for t.OctaveUpBtn.Clicked() { - t.ChangeOctave(1) - } - for t.OctaveDownBtn.Clicked() { - t.ChangeOctave(-1) - } - for t.BPMUpBtn.Clicked() { - t.ChangeBPM(1) - } - for t.BPMDownBtn.Clicked() { - t.ChangeBPM(-1) - } + go func() { + for t.OctaveUpBtn.Clicked() { + t.ChangeOctave(1) + } + for t.OctaveDownBtn.Clicked() { + t.ChangeOctave(-1) + } + for t.BPMUpBtn.Clicked() { + t.ChangeBPM(1) + } + for t.BPMDownBtn.Clicked() { + t.ChangeBPM(-1) + } + for t.NewInstrumentBtn.Clicked() { + t.AddInstrument() + } + }() return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, layout.Rigid(Raised(t.layoutPatterns( @@ -128,6 +161,10 @@ func (t *Tracker) layoutControls(gtx layout.Context) layout.Dimensions { layout.Rigid(func(gtx layout.Context) layout.Dimensions { return in.Layout(gtx, material.IconButton(t.Theme, t.BPMDownBtn, downIcon).Layout) }), + layout.Rigid(t.darkLine(false)), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return in.Layout(gtx, material.IconButton(t.Theme, t.NewInstrumentBtn, addIcon).Layout) + }), ) } diff --git a/tracker/patterns.go b/tracker/patterns.go index e3080d8..e5a947e 100644 --- a/tracker/patterns.go +++ b/tracker/patterns.go @@ -13,7 +13,7 @@ import ( "github.com/vsariola/sointu" ) -const patternCellHeight = 12 +const patternCellHeight = 16 const patternCellWidth = 16 func (t *Tracker) layoutPatterns(tracks []sointu.Track, activeTrack, cursorPattern, cursorCol, playingPattern int) layout.Widget { diff --git a/tracker/sequencer.go b/tracker/sequencer.go index 616e467..fc3b257 100644 --- a/tracker/sequencer.go +++ b/tracker/sequencer.go @@ -57,7 +57,9 @@ func (s *Sequencer) ReadAudio(buffer []float32) (int, error) { gotRow := true if s.rowTime >= s.rowLength { var row []Note + s.mutex.Unlock() row, gotRow = s.iterator() + s.mutex.Lock() if gotRow { for _, n := range row { s.doNote(n.Voice, n.Note) diff --git a/tracker/theme.go b/tracker/theme.go index e255f0b..6280df8 100644 --- a/tracker/theme.go +++ b/tracker/theme.go @@ -11,7 +11,7 @@ import ( var fontCollection []text.FontFace = gofont.Collection() var textShaper = text.NewCache(fontCollection) -var neutral = color.RGBA{R: 64, G: 64, B: 64, A: 255} +var neutral = color.RGBA{R: 38, G: 38, B: 38, A: 255} var light = color.RGBA{R: 128, G: 128, B: 128, A: 255} var dark = color.RGBA{R: 15, G: 15, B: 15, A: 255} var white = color.RGBA{R: 255, G: 255, B: 255, A: 255} @@ -48,3 +48,5 @@ var patternTextColor = white var patternActiveTextColor = yellow var patternFont = fontCollection[6].Font var patternFontSize = unit.Px(12) + +var inactiveBtnColor = color.RGBA{R: 61, G: 55, B: 55, A: 255} diff --git a/tracker/tracker.go b/tracker/tracker.go index 63867bc..66878c9 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -18,20 +18,22 @@ 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 + 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 sequencer *Sequencer ticked chan struct{} @@ -85,11 +87,11 @@ func (t *Tracker) sequencerLoop(closer <-chan struct{}) { } curVoices := make([]int, 32) t.sequencer = NewSequencer(synth, 44100*60/(4*t.song.BPM), func() ([]Note, bool) { + t.playRowPatMutex.Lock() if !t.Playing { + t.playRowPatMutex.Unlock() return nil, false } - t.playRowPatMutex.Lock() - defer t.playRowPatMutex.Unlock() t.PlayRow++ if t.PlayRow >= t.song.PatternRows() { t.PlayRow = 0 @@ -109,16 +111,17 @@ func (t *Tracker) sequencerLoop(closer <-chan struct{}) { if note == 1 { // anything but hold causes an action. continue } - notes = append(notes, Note{curVoices[track], 0}) + first := t.song.FirstTrackVoice(track) + notes = append(notes, Note{first + curVoices[track], 0}) if note > 1 { curVoices[track]++ - first := t.song.FirstTrackVoice(track) - if curVoices[track] >= first+t.song.Tracks[track].NumVoices { - curVoices[track] = first + if curVoices[track] >= t.song.Tracks[track].NumVoices { + curVoices[track] = 0 } - notes = append(notes, Note{curVoices[track], note}) + notes = append(notes, Note{first + curVoices[track], note}) } } + t.playRowPatMutex.Unlock() t.ticked <- struct{}{} return notes, true }) @@ -165,21 +168,58 @@ func (t *Tracker) ChangeBPM(delta int) bool { return false } +func (t *Tracker) AddTrack() { + if t.song.TotalTrackVoices() < t.song.Patch.TotalVoices() { + seq := make([]byte, t.song.SequenceLength()) + patterns := [][]byte{make([]byte, t.song.PatternRows())} + t.song.Tracks = append(t.song.Tracks, sointu.Track{ + NumVoices: 1, + Patterns: patterns, + Sequence: seq, + }) + } +} + +func (t *Tracker) AddInstrument() { + if t.song.Patch.TotalVoices() < 32 { + units := make([]sointu.Unit, len(defaultInstrument.Units)) + for i, defUnit := range defaultInstrument.Units { + units[i].Type = defUnit.Type + units[i].Parameters = make(map[string]int) + for k, v := range defUnit.Parameters { + units[i].Parameters[k] = v + } + } + t.song.Patch.Instruments = append(t.song.Patch.Instruments, sointu.Instrument{ + NumVoices: defaultInstrument.NumVoices, + Units: units, + }) + } + synth, err := bridge.Synth(t.song.Patch) + if err == nil { + t.sequencer.SetSynth(synth) + } else { + fmt.Printf("%v", err) + } +} + func New(audioContext sointu.AudioContext) *Tracker { t := &Tracker{ - Theme: material.NewTheme(gofont.Collection()), - QuitButton: new(widget.Clickable), - CurrentOctave: 4, - audioContext: audioContext, - OctaveUpBtn: new(widget.Clickable), - OctaveDownBtn: new(widget.Clickable), - BPMUpBtn: new(widget.Clickable), - BPMDownBtn: new(widget.Clickable), - setPlaying: make(chan bool), - rowJump: make(chan int), - patternJump: make(chan int), - ticked: make(chan struct{}), - closer: make(chan struct{}), + Theme: material.NewTheme(gofont.Collection()), + QuitButton: new(widget.Clickable), + CurrentOctave: 4, + audioContext: audioContext, + OctaveUpBtn: new(widget.Clickable), + OctaveDownBtn: new(widget.Clickable), + BPMUpBtn: new(widget.Clickable), + BPMDownBtn: new(widget.Clickable), + NewTrackBtn: new(widget.Clickable), + NewInstrumentBtn: new(widget.Clickable), + setPlaying: make(chan bool), + rowJump: make(chan int), + patternJump: make(chan int), + ticked: make(chan struct{}), + closer: make(chan struct{}), } t.Theme.Color.Primary = color.RGBA{R: 64, G: 64, B: 64, A: 255} go t.sequencerLoop(t.closer)