mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
feat(tracker): add new instrument & new track buttons
This commit is contained in:
parent
e480622f57
commit
cbf9d34738
@ -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 {
|
||||
|
@ -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{
|
||||
|
@ -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)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user