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
}
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 {

View File

@ -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{

View File

@ -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)
}),
)
}

View File

@ -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 {

View File

@ -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)

View File

@ -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}

View File

@ -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)