sointu/tracker/tracker.go

502 lines
14 KiB
Go

package tracker
import (
"fmt"
"sync"
"gioui.org/font/gofont"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/vsariola/sointu"
)
type Tracker struct {
QuitButton *widget.Clickable
songPlayMutex sync.RWMutex // protects song and playing
song sointu.Song
Playing bool
// protects PlayPattern and PlayRow
playRowPatMutex sync.RWMutex // protects song and playing
PlayPosition SongRow
SelectionCorner SongPoint
Cursor SongPoint
CursorColumn int
CurrentInstrument int
CurrentUnit int
UnitGroupMenuVisible bool
UnitGroupMenuIndex int
UnitSubMenuIndex int
NoteTracking bool
Theme *material.Theme
Octave *NumberInput
BPM *NumberInput
RowsPerPattern *NumberInput
RowsPerBeat *NumberInput
InstrumentVoices *NumberInput
NewTrackBtn *widget.Clickable
NewInstrumentBtn *widget.Clickable
DeleteInstrumentBtn *widget.Clickable
LoadSongFileBtn *widget.Clickable
NewSongFileBtn *widget.Clickable
AddSemitoneBtn *widget.Clickable
SubtractSemitoneBtn *widget.Clickable
AddOctaveBtn *widget.Clickable
SubtractOctaveBtn *widget.Clickable
SongLength *NumberInput
SaveSongFileBtn *widget.Clickable
FileMenuBtn *widget.Clickable
FileMenuVisible bool
ParameterSliders []*widget.Float
UnitBtns []*widget.Clickable
UnitList *layout.List
DeleteUnitBtn *widget.Clickable
ClearUnitBtn *widget.Clickable
ChooseUnitTypeList *layout.List
ChooseUnitTypeBtns []*widget.Clickable
InstrumentBtns []*widget.Clickable
AddUnitBtn *widget.Clickable
InstrumentList *layout.List
TrackHexCheckBoxes []*widget.Bool
TrackShowHex []bool
TopHorizontalSplit *Split
BottomHorizontalSplit *Split
VerticalSplit *Split
sequencer *Sequencer
ticked chan struct{}
setPlaying chan bool
rowJump chan int
patternJump chan int
audioContext sointu.AudioContext
playBuffer []float32
closer chan struct{}
undoStack []sointu.Song
redoStack []sointu.Song
}
func (t *Tracker) LoadSong(song sointu.Song) error {
if err := song.Validate(); err != nil {
return fmt.Errorf("invalid song: %w", err)
}
t.songPlayMutex.Lock()
defer t.songPlayMutex.Unlock()
t.song = song
t.ClampPositions()
if t.sequencer != nil {
t.sequencer.SetPatch(song.Patch)
t.sequencer.SetRowLength(song.SamplesPerRow())
}
return nil
}
func (t *Tracker) Close() {
t.audioContext.Close()
t.closer <- struct{}{}
}
func (t *Tracker) TogglePlay() {
t.songPlayMutex.Lock()
defer t.songPlayMutex.Unlock()
t.Playing = !t.Playing
if t.Playing {
t.NoteTracking = true
t.PlayPosition = t.Cursor.SongRow
t.PlayPosition.Row-- // TODO: we advance soon to make up for this -1, but this is not very elegant way to do it
}
}
func (t *Tracker) sequencerLoop(closer <-chan struct{}) {
output := t.audioContext.Output()
defer output.Close()
buffer := make([]float32, 8192)
for {
select {
case <-closer:
return
default:
read, _ := t.sequencer.ReadAudio(buffer)
for read < len(buffer) {
buffer[read] = 0
read++
}
output.WriteAudio(buffer)
}
}
}
func (t *Tracker) ChangeOctave(delta int) bool {
newOctave := t.Octave.Value + delta
if newOctave < 0 {
newOctave = 0
}
if newOctave > 9 {
newOctave = 9
}
if newOctave != t.Octave.Value {
t.Octave.Value = newOctave
return true
}
return false
}
func (t *Tracker) SetInstrumentVoices(value int) bool {
if value < 1 {
value = 1
}
maxRemain := 32 - t.song.Patch.TotalVoices() + t.song.Patch.Instruments[t.CurrentInstrument].NumVoices
if maxRemain < 1 {
maxRemain = 1
}
if value > maxRemain {
value = maxRemain
}
if value != int(t.song.Patch.Instruments[t.CurrentInstrument].NumVoices) {
t.SaveUndo()
t.song.Patch.Instruments[t.CurrentInstrument].NumVoices = value
t.sequencer.SetPatch(t.song.Patch)
return true
}
return false
}
func (t *Tracker) SetBPM(value int) bool {
if value < 1 {
value = 1
}
if value > 999 {
value = 999
}
if value != int(t.song.BPM) {
t.SaveUndo()
t.song.BPM = value
t.sequencer.SetRowLength(t.song.SamplesPerRow())
return true
}
return false
}
func (t *Tracker) SetRowsPerBeat(value int) bool {
if value < 1 {
value = 1
}
if value > 32 {
value = 32
}
if value != int(t.song.RowsPerBeat) {
t.SaveUndo()
t.song.RowsPerBeat = value
t.sequencer.SetRowLength(t.song.SamplesPerRow())
return true
}
return false
}
func (t *Tracker) AddTrack() {
t.SaveUndo()
if t.song.TotalTrackVoices() < t.song.Patch.TotalVoices() {
seq := make([]byte, t.song.SequenceLength())
patterns := [][]byte{make([]byte, t.song.RowsPerPattern)}
t.song.Tracks = append(t.song.Tracks, sointu.Track{
NumVoices: 1,
Patterns: patterns,
Sequence: seq,
})
}
}
func (t *Tracker) AddInstrument() {
t.SaveUndo()
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,
})
}
t.sequencer.SetPatch(t.song.Patch)
}
func (t *Tracker) DeleteInstrument() {
if len(t.song.Patch.Instruments) <= 1 {
return
}
t.SaveUndo()
t.song.Patch.Instruments = append(t.song.Patch.Instruments[:t.CurrentInstrument], t.song.Patch.Instruments[t.CurrentInstrument+1:]...)
if t.CurrentInstrument >= len(t.song.Patch.Instruments) {
t.CurrentInstrument = len(t.song.Patch.Instruments) - 1
}
t.sequencer.SetPatch(t.song.Patch)
}
// SetCurrentNote sets the (note) value in current pattern under cursor to iv
func (t *Tracker) SetCurrentNote(iv byte) {
t.SaveUndo()
t.song.Tracks[t.Cursor.Track].Patterns[t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern]][t.Cursor.Row] = iv
}
func (t *Tracker) SetCurrentPattern(pat byte) {
t.SaveUndo()
length := len(t.song.Tracks[t.Cursor.Track].Patterns)
if int(pat) >= length {
tail := make([][]byte, int(pat)-length+1)
for i := range tail {
tail[i] = make([]byte, t.song.RowsPerPattern)
}
t.song.Tracks[t.Cursor.Track].Patterns = append(t.song.Tracks[t.Cursor.Track].Patterns, tail...)
}
t.song.Tracks[t.Cursor.Track].Sequence[t.Cursor.Pattern] = pat
}
func (t *Tracker) SetSongLength(value int) {
if value < 1 {
value = 1
}
if value != t.song.SequenceLength() {
t.SaveUndo()
for i := range t.song.Tracks {
seq := t.song.Tracks[i].Sequence
if len(t.song.Tracks[i].Sequence) > value {
t.song.Tracks[i].Sequence = t.song.Tracks[i].Sequence[:value]
} else if len(t.song.Tracks[i].Sequence) < value {
for k := len(t.song.Tracks[i].Sequence); k < value; k++ {
t.song.Tracks[i].Sequence = append(seq, seq[len(seq)-1])
}
}
}
t.ClampPositions()
}
}
func (t *Tracker) SetRowsPerPattern(value int) {
if value < 1 {
value = 1
}
if value > 255 {
value = 255
}
if value != t.song.RowsPerPattern {
t.SaveUndo()
for i := range t.song.Tracks {
for j := range t.song.Tracks[i].Patterns {
pat := t.song.Tracks[i].Patterns[j]
if l := len(pat); l < value {
tail := make([]byte, value-l)
for k := range tail {
tail[k] = 1
}
t.song.Tracks[i].Patterns[j] = append(pat, tail...)
}
}
}
t.song.RowsPerPattern = value
t.ClampPositions()
}
}
func (t *Tracker) SetUnit(typ string) {
unit, ok := defaultUnits[typ]
if !ok {
return
}
if unit.Type == t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type {
return
}
t.SaveUndo()
t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit] = unit.Copy()
t.sequencer.SetPatch(t.song.Patch)
}
func (t *Tracker) AddUnit() {
t.SaveUndo()
units := make([]sointu.Unit, len(t.song.Patch.Instruments[t.CurrentInstrument].Units)+1)
copy(units, t.song.Patch.Instruments[t.CurrentInstrument].Units[:t.CurrentUnit+1])
copy(units[t.CurrentUnit+2:], t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit+1:])
t.song.Patch.Instruments[t.CurrentInstrument].Units = units
t.CurrentUnit++
t.sequencer.SetPatch(t.song.Patch)
}
func (t *Tracker) ClearUnit() {
t.SaveUndo()
t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Type = ""
t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit].Parameters = make(map[string]int)
t.sequencer.SetPatch(t.song.Patch)
}
func (t *Tracker) DeleteUnit() {
if len(t.song.Patch.Instruments[t.CurrentInstrument].Units) <= 1 {
return
}
t.SaveUndo()
units := make([]sointu.Unit, len(t.song.Patch.Instruments[t.CurrentInstrument].Units)-1)
copy(units, t.song.Patch.Instruments[t.CurrentInstrument].Units[:t.CurrentUnit])
copy(units[t.CurrentUnit:], t.song.Patch.Instruments[t.CurrentInstrument].Units[t.CurrentUnit+1:])
t.song.Patch.Instruments[t.CurrentInstrument].Units = units
if t.CurrentUnit > 0 {
t.CurrentUnit--
}
t.sequencer.SetPatch(t.song.Patch)
}
func (t *Tracker) ClampPositions() {
t.PlayPosition.Clamp(t.song)
t.Cursor.Clamp(t.song)
t.SelectionCorner.Clamp(t.song)
}
func (t *Tracker) getSelectionRange() (int, int, int, int) {
r1 := t.Cursor.Pattern*t.song.RowsPerPattern + t.Cursor.Row
r2 := t.SelectionCorner.Pattern*t.song.RowsPerPattern + t.SelectionCorner.Row
if r2 < r1 {
r1, r2 = r2, r1
}
t1 := t.Cursor.Track
t2 := t.SelectionCorner.Track
if t2 < t1 {
t1, t2 = t2, t1
}
return r1, r2, t1, t2
}
func (t *Tracker) AdjustSelectionPitch(delta int) {
t.SaveUndo()
r1, r2, t1, t2 := t.getSelectionRange()
for c := t1; c <= t2; c++ {
adjustedNotes := map[struct {
Pat byte
Row int
}]bool{}
for r := r1; r <= r2; r++ {
s := SongRow{Row: r}
s.Wrap(t.song)
p := t.song.Tracks[c].Sequence[s.Pattern]
noteIndex := struct {
Pat byte
Row int
}{p, s.Row}
if !adjustedNotes[noteIndex] {
if val := t.song.Tracks[c].Patterns[p][s.Row]; val > 1 {
newVal := int(val) + delta
if newVal < 2 {
newVal = 2
} else if newVal > 255 {
newVal = 255
}
t.song.Tracks[c].Patterns[p][s.Row] = byte(newVal)
}
adjustedNotes[noteIndex] = true
}
}
}
}
func (t *Tracker) DeleteSelection() {
t.SaveUndo()
r1, r2, t1, t2 := t.getSelectionRange()
for r := r1; r <= r2; r++ {
s := SongRow{Row: r}
s.Wrap(t.song)
for c := t1; c <= t2; c++ {
p := t.song.Tracks[c].Sequence[s.Pattern]
t.song.Tracks[c].Patterns[p][s.Row] = 1
}
}
}
func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tracker {
t := &Tracker{
Theme: material.NewTheme(gofont.Collection()),
QuitButton: new(widget.Clickable),
audioContext: audioContext,
BPM: new(NumberInput),
Octave: new(NumberInput),
SongLength: new(NumberInput),
RowsPerPattern: new(NumberInput),
RowsPerBeat: new(NumberInput),
InstrumentVoices: new(NumberInput),
NewTrackBtn: new(widget.Clickable),
NewInstrumentBtn: new(widget.Clickable),
DeleteInstrumentBtn: new(widget.Clickable),
NewSongFileBtn: new(widget.Clickable),
FileMenuBtn: new(widget.Clickable),
LoadSongFileBtn: new(widget.Clickable),
SaveSongFileBtn: new(widget.Clickable),
AddSemitoneBtn: new(widget.Clickable),
SubtractSemitoneBtn: new(widget.Clickable),
AddOctaveBtn: new(widget.Clickable),
SubtractOctaveBtn: new(widget.Clickable),
AddUnitBtn: new(widget.Clickable),
DeleteUnitBtn: new(widget.Clickable),
ClearUnitBtn: new(widget.Clickable),
UnitList: &layout.List{Axis: layout.Vertical},
setPlaying: make(chan bool),
rowJump: make(chan int),
patternJump: make(chan int),
ticked: make(chan struct{}),
closer: make(chan struct{}),
undoStack: []sointu.Song{},
redoStack: []sointu.Song{},
InstrumentList: &layout.List{Axis: layout.Horizontal},
TopHorizontalSplit: new(Split),
BottomHorizontalSplit: new(Split),
VerticalSplit: new(Split),
ChooseUnitTypeList: &layout.List{Axis: layout.Vertical},
}
t.Octave.Value = 4
t.VerticalSplit.Axis = layout.Vertical
t.BottomHorizontalSplit.Ratio = -.5
t.Theme.Palette.Fg = primaryColor
t.Theme.Palette.ContrastFg = black
for range allUnits {
t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable))
}
curVoices := make([]int, 32)
t.sequencer = NewSequencer(synthService, func() ([]Note, bool) {
t.playRowPatMutex.Lock()
if !t.Playing {
t.playRowPatMutex.Unlock()
return nil, false
}
t.PlayPosition.Row++
t.PlayPosition.Wrap(t.song)
if t.NoteTracking {
t.Cursor.SongRow = t.PlayPosition
t.SelectionCorner.SongRow = t.PlayPosition
}
notes := make([]Note, 0, 32)
for track := range t.song.Tracks {
patternIndex := t.song.Tracks[track].Sequence[t.PlayPosition.Pattern]
note := t.song.Tracks[track].Patterns[patternIndex][t.PlayPosition.Row]
if note == 1 { // anything but hold causes an action.
continue
}
first := t.song.FirstTrackVoice(track)
notes = append(notes, Note{first + curVoices[track], 0})
if note > 1 {
curVoices[track]++
if curVoices[track] >= t.song.Tracks[track].NumVoices {
curVoices[track] = 0
}
notes = append(notes, Note{first + curVoices[track], note})
}
}
t.playRowPatMutex.Unlock()
t.ticked <- struct{}{}
return notes, true
})
go t.sequencerLoop(t.closer)
if err := t.LoadSong(defaultSong.Copy()); err != nil {
panic(fmt.Errorf("cannot load default song: %w", err))
}
return t
}