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,6 +115,7 @@ func (t *Tracker) layoutControls(gtx layout.Context) layout.Dimensions {
} }
in := layout.UniformInset(unit.Dp(8)) in := layout.UniformInset(unit.Dp(8))
go func() {
for t.OctaveUpBtn.Clicked() { for t.OctaveUpBtn.Clicked() {
t.ChangeOctave(1) t.ChangeOctave(1)
} }
@ -99,6 +128,10 @@ func (t *Tracker) layoutControls(gtx layout.Context) layout.Dimensions {
for t.BPMDownBtn.Clicked() { for t.BPMDownBtn.Clicked() {
t.ChangeBPM(-1) 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

@ -32,6 +32,8 @@ type Tracker struct {
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,6 +168,41 @@ 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()),
@ -175,6 +213,8 @@ func New(audioContext sointu.AudioContext) *Tracker {
OctaveDownBtn: new(widget.Clickable), OctaveDownBtn: new(widget.Clickable),
BPMUpBtn: new(widget.Clickable), BPMUpBtn: new(widget.Clickable),
BPMDownBtn: new(widget.Clickable), BPMDownBtn: new(widget.Clickable),
NewTrackBtn: new(widget.Clickable),
NewInstrumentBtn: new(widget.Clickable),
setPlaying: make(chan bool), setPlaying: make(chan bool),
rowJump: make(chan int), rowJump: make(chan int),
patternJump: make(chan int), patternJump: make(chan int),