mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-25 18:00:37 -04:00
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.
173 lines
6.0 KiB
Go
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
|
|
}
|