sointu/tracker/gioui/tracker.go
vsariola 99dbdfe223 feat: add the ability to use Sointu as a sync-tracker
There is a new "sync" opcode that saves the top-most signal every 256 samples to the new "syncBuffer" output. Additionally, you can enable saving the current fractional row as sync[0], avoiding calculating the beat in the shader, but also calculating the beat correctly when the beat is modulated.
2021-03-09 23:52:33 +02:00

173 lines
6.0 KiB
Go

package gioui
import (
"encoding/json"
"errors"
"fmt"
"gioui.org/font/gofont"
"gioui.org/layout"
"gioui.org/text"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/tracker"
"gopkg.in/yaml.v3"
)
type Tracker struct {
Theme *material.Theme
MenuBar []widget.Clickable
Menus []Menu
OctaveNumberInput *NumberInput
BPM *NumberInput
RowsPerPattern *NumberInput
RowsPerBeat *NumberInput
Step *NumberInput
InstrumentVoices *NumberInput
TrackVoices *NumberInput
InstrumentNameEditor *widget.Editor
NewTrackBtn *widget.Clickable
NewInstrumentBtn *widget.Clickable
DeleteInstrumentBtn *widget.Clickable
AddSemitoneBtn *widget.Clickable
SubtractSemitoneBtn *widget.Clickable
AddOctaveBtn *widget.Clickable
SubtractOctaveBtn *widget.Clickable
SongLength *NumberInput
PanicBtn *widget.Clickable
CopyInstrumentBtn *widget.Clickable
ParameterList *layout.List
ParameterScrollBar *ScrollBar
Parameters []*ParameterWidget
UnitDragList *DragList
UnitScrollBar *ScrollBar
DeleteUnitBtn *widget.Clickable
ClearUnitBtn *widget.Clickable
ChooseUnitTypeList *layout.List
ChooseUnitScrollBar *ScrollBar
ChooseUnitTypeBtns []*widget.Clickable
AddUnitBtn *widget.Clickable
InstrumentDragList *DragList
InstrumentScrollBar *ScrollBar
TrackHexCheckBox *widget.Bool
TopHorizontalSplit *Split
BottomHorizontalSplit *Split
VerticalSplit *Split
StackUse []int
KeyPlaying map[string]uint32
Alert Alert
lastVolume tracker.Volume
volumeChan chan tracker.Volume
player *tracker.Player
refresh chan struct{}
playerCloser chan struct{}
audioContext sointu.AudioContext
*tracker.Model
}
func (t *Tracker) UnmarshalContent(bytes []byte) error {
var instr sointu.Instrument
if errJSON := json.Unmarshal(bytes, &instr); errJSON == nil {
if t.SetInstrument(instr) {
return nil
}
}
if errYaml := yaml.Unmarshal(bytes, &instr); errYaml == nil {
if t.SetInstrument(instr) {
return nil
}
}
var song sointu.Song
if errJSON := json.Unmarshal(bytes, &song); errJSON != nil {
if errYaml := yaml.Unmarshal(bytes, &song); errYaml != nil {
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
}
}
if song.BPM > 0 {
t.SetSong(song)
return nil
}
return errors.New("was able to unmarshal a song, but the bpm was 0")
}
func (t *Tracker) Close() {
t.playerCloser <- struct{}{}
t.audioContext.Close()
}
func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syncChannel chan<- []float32) *Tracker {
t := &Tracker{
Theme: material.NewTheme(gofont.Collection()),
audioContext: audioContext,
BPM: new(NumberInput),
OctaveNumberInput: &NumberInput{Value: 4},
SongLength: new(NumberInput),
RowsPerPattern: new(NumberInput),
RowsPerBeat: new(NumberInput),
Step: &NumberInput{Value: 1},
InstrumentVoices: new(NumberInput),
TrackVoices: new(NumberInput),
InstrumentNameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle},
NewTrackBtn: new(widget.Clickable),
NewInstrumentBtn: new(widget.Clickable),
DeleteInstrumentBtn: 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),
PanicBtn: new(widget.Clickable),
CopyInstrumentBtn: new(widget.Clickable),
TrackHexCheckBox: new(widget.Bool),
Menus: make([]Menu, 2),
MenuBar: make([]widget.Clickable, 2),
UnitDragList: &DragList{List: &layout.List{Axis: layout.Vertical}, HoverItem: -1},
UnitScrollBar: &ScrollBar{Axis: layout.Vertical},
refresh: make(chan struct{}, 1), // use non-blocking sends; no need to queue extra ticks if one is queued already
InstrumentDragList: &DragList{List: &layout.List{Axis: layout.Horizontal}, HoverItem: -1},
InstrumentScrollBar: &ScrollBar{Axis: layout.Horizontal},
ParameterList: &layout.List{Axis: layout.Vertical},
ParameterScrollBar: &ScrollBar{Axis: layout.Vertical},
TopHorizontalSplit: &Split{Ratio: -.6},
BottomHorizontalSplit: &Split{Ratio: -.6},
VerticalSplit: &Split{Axis: layout.Vertical},
ChooseUnitTypeList: &layout.List{Axis: layout.Vertical},
ChooseUnitScrollBar: &ScrollBar{Axis: layout.Vertical},
KeyPlaying: make(map[string]uint32),
volumeChan: make(chan tracker.Volume, 1),
playerCloser: make(chan struct{}),
}
t.Model = tracker.NewModel()
vuBufferObserver := make(chan []float32)
go tracker.VuAnalyzer(0.3, 1e-4, 1, -100, vuBufferObserver, t.volumeChan)
t.Theme.Palette.Fg = primaryColor
t.Theme.Palette.ContrastFg = black
t.SetEditMode(tracker.EditTracks)
for range tracker.UnitTypeNames {
t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable))
}
t.SetOctave(4)
patchObserver := make(chan sointu.Patch, 16)
t.AddPatchObserver(patchObserver)
scoreObserver := make(chan sointu.Score, 16)
t.AddScoreObserver(scoreObserver)
sprObserver := make(chan int, 16)
t.AddSamplesPerRowObserver(sprObserver)
audioChannel := make(chan []float32)
t.player = tracker.NewPlayer(synthService, t.playerCloser, patchObserver, scoreObserver, sprObserver, t.refresh, syncChannel, audioChannel, vuBufferObserver)
audioOut := audioContext.Output()
go func() {
for buf := range audioChannel {
audioOut.WriteAudio(buf)
}
}()
t.ResetSong()
return t
}