feat(tracker): add new instrument & new track buttons

This commit is contained in:
vsariola 2021-01-08 18:55:02 +02:00
parent e480622f57
commit cbf9d34738
7 changed files with 151 additions and 52 deletions

View File

@ -241,6 +241,14 @@ func (s *Song) FirstTrackVoice(track int) int {
return ret 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 // TBD: Where shall we put methods that work on pure domain types and have no dependencies
// e.g. Validate here // e.g. Validate here
func (s *Song) Validate() error { func (s *Song) Validate() error {

View File

@ -2,6 +2,16 @@ package tracker
import "github.com/vsariola/sointu" 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{ var defaultSong = sointu.Song{
BPM: 100, BPM: 100,
Tracks: []sointu.Track{ Tracks: []sointu.Track{

View File

@ -18,6 +18,7 @@ import (
var upIcon *widget.Icon var upIcon *widget.Icon
var downIcon *widget.Icon var downIcon *widget.Icon
var addIcon *widget.Icon
func init() { func init() {
var err error var err error
@ -29,6 +30,10 @@ func init() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
addIcon, err = widget.NewIcon(icons.ContentAdd)
if err != nil {
log.Fatal(err)
}
} }
func (t *Tracker) Layout(gtx layout.Context) { 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 { 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() t.playRowPatMutex.RLock()
defer t.playRowPatMutex.RUnlock() defer t.playRowPatMutex.RUnlock()
@ -51,7 +56,7 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
playPat = -1 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].Patterns[0]),
len(t.song.Tracks[0].Sequence), len(t.song.Tracks[0].Sequence),
t.CursorRow, t.CursorRow,
@ -61,7 +66,7 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
playPat, playPat,
))) )))
for i, trk := range t.song.Tracks { 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.Patterns,
trk.Sequence, trk.Sequence,
t.ActiveTrack == i, t.ActiveTrack == i,
@ -72,8 +77,31 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
playPat, 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, 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)) in := layout.UniformInset(unit.Dp(8))
for t.OctaveUpBtn.Clicked() { go func() {
t.ChangeOctave(1) for t.OctaveUpBtn.Clicked() {
} t.ChangeOctave(1)
for t.OctaveDownBtn.Clicked() { }
t.ChangeOctave(-1) for t.OctaveDownBtn.Clicked() {
} t.ChangeOctave(-1)
for t.BPMUpBtn.Clicked() { }
t.ChangeBPM(1) for t.BPMUpBtn.Clicked() {
} t.ChangeBPM(1)
for t.BPMDownBtn.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, return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(Raised(t.layoutPatterns( 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 { layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return in.Layout(gtx, material.IconButton(t.Theme, t.BPMDownBtn, downIcon).Layout) 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)
}),
) )
} }

View File

@ -13,7 +13,7 @@ import (
"github.com/vsariola/sointu" "github.com/vsariola/sointu"
) )
const patternCellHeight = 12 const patternCellHeight = 16
const patternCellWidth = 16 const patternCellWidth = 16
func (t *Tracker) layoutPatterns(tracks []sointu.Track, activeTrack, cursorPattern, cursorCol, playingPattern int) layout.Widget { func (t *Tracker) layoutPatterns(tracks []sointu.Track, activeTrack, cursorPattern, cursorCol, playingPattern int) layout.Widget {

View File

@ -57,7 +57,9 @@ func (s *Sequencer) ReadAudio(buffer []float32) (int, error) {
gotRow := true gotRow := true
if s.rowTime >= s.rowLength { if s.rowTime >= s.rowLength {
var row []Note var row []Note
s.mutex.Unlock()
row, gotRow = s.iterator() row, gotRow = s.iterator()
s.mutex.Lock()
if gotRow { if gotRow {
for _, n := range row { for _, n := range row {
s.doNote(n.Voice, n.Note) s.doNote(n.Voice, n.Note)

View File

@ -11,7 +11,7 @@ import (
var fontCollection []text.FontFace = gofont.Collection() var fontCollection []text.FontFace = gofont.Collection()
var textShaper = text.NewCache(fontCollection) 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 light = color.RGBA{R: 128, G: 128, B: 128, A: 255}
var dark = color.RGBA{R: 15, G: 15, B: 15, 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} var white = color.RGBA{R: 255, G: 255, B: 255, A: 255}
@ -48,3 +48,5 @@ var patternTextColor = white
var patternActiveTextColor = yellow var patternActiveTextColor = yellow
var patternFont = fontCollection[6].Font var patternFont = fontCollection[6].Font
var patternFontSize = unit.Px(12) var patternFontSize = unit.Px(12)
var inactiveBtnColor = color.RGBA{R: 61, G: 55, B: 55, A: 255}

View File

@ -18,20 +18,22 @@ type Tracker struct {
song sointu.Song song sointu.Song
Playing bool Playing bool
// protects PlayPattern and PlayRow // protects PlayPattern and PlayRow
playRowPatMutex sync.RWMutex // protects song and playing playRowPatMutex sync.RWMutex // protects song and playing
PlayPattern int PlayPattern int
PlayRow int PlayRow int
CursorRow int CursorRow int
CursorColumn int CursorColumn int
DisplayPattern int DisplayPattern int
ActiveTrack int ActiveTrack int
CurrentOctave byte CurrentOctave byte
NoteTracking bool NoteTracking bool
Theme *material.Theme Theme *material.Theme
OctaveUpBtn *widget.Clickable OctaveUpBtn *widget.Clickable
OctaveDownBtn *widget.Clickable OctaveDownBtn *widget.Clickable
BPMUpBtn *widget.Clickable BPMUpBtn *widget.Clickable
BPMDownBtn *widget.Clickable BPMDownBtn *widget.Clickable
NewTrackBtn *widget.Clickable
NewInstrumentBtn *widget.Clickable
sequencer *Sequencer sequencer *Sequencer
ticked chan struct{} ticked chan struct{}
@ -85,11 +87,11 @@ func (t *Tracker) sequencerLoop(closer <-chan struct{}) {
} }
curVoices := make([]int, 32) curVoices := make([]int, 32)
t.sequencer = NewSequencer(synth, 44100*60/(4*t.song.BPM), func() ([]Note, bool) { t.sequencer = NewSequencer(synth, 44100*60/(4*t.song.BPM), func() ([]Note, bool) {
t.playRowPatMutex.Lock()
if !t.Playing { if !t.Playing {
t.playRowPatMutex.Unlock()
return nil, false return nil, false
} }
t.playRowPatMutex.Lock()
defer t.playRowPatMutex.Unlock()
t.PlayRow++ t.PlayRow++
if t.PlayRow >= t.song.PatternRows() { if t.PlayRow >= t.song.PatternRows() {
t.PlayRow = 0 t.PlayRow = 0
@ -109,16 +111,17 @@ func (t *Tracker) sequencerLoop(closer <-chan struct{}) {
if note == 1 { // anything but hold causes an action. if note == 1 { // anything but hold causes an action.
continue continue
} }
notes = append(notes, Note{curVoices[track], 0}) first := t.song.FirstTrackVoice(track)
notes = append(notes, Note{first + curVoices[track], 0})
if note > 1 { if note > 1 {
curVoices[track]++ curVoices[track]++
first := t.song.FirstTrackVoice(track) if curVoices[track] >= t.song.Tracks[track].NumVoices {
if curVoices[track] >= first+t.song.Tracks[track].NumVoices { curVoices[track] = 0
curVoices[track] = first
} }
notes = append(notes, Note{curVoices[track], note}) notes = append(notes, Note{first + curVoices[track], note})
} }
} }
t.playRowPatMutex.Unlock()
t.ticked <- struct{}{} t.ticked <- struct{}{}
return notes, true return notes, true
}) })
@ -165,21 +168,58 @@ func (t *Tracker) ChangeBPM(delta int) bool {
return false 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 { func New(audioContext sointu.AudioContext) *Tracker {
t := &Tracker{ t := &Tracker{
Theme: material.NewTheme(gofont.Collection()), Theme: material.NewTheme(gofont.Collection()),
QuitButton: new(widget.Clickable), QuitButton: new(widget.Clickable),
CurrentOctave: 4, CurrentOctave: 4,
audioContext: audioContext, audioContext: audioContext,
OctaveUpBtn: new(widget.Clickable), OctaveUpBtn: new(widget.Clickable),
OctaveDownBtn: new(widget.Clickable), OctaveDownBtn: new(widget.Clickable),
BPMUpBtn: new(widget.Clickable), BPMUpBtn: new(widget.Clickable),
BPMDownBtn: new(widget.Clickable), BPMDownBtn: new(widget.Clickable),
setPlaying: make(chan bool), NewTrackBtn: new(widget.Clickable),
rowJump: make(chan int), NewInstrumentBtn: new(widget.Clickable),
patternJump: make(chan int), setPlaying: make(chan bool),
ticked: make(chan struct{}), rowJump: make(chan int),
closer: make(chan struct{}), 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} t.Theme.Color.Primary = color.RGBA{R: 64, G: 64, B: 64, A: 255}
go t.sequencerLoop(t.closer) go t.sequencerLoop(t.closer)