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
|
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 {
|
||||||
|
@ -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{
|
||||||
|
@ -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)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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}
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user