mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
feat(tracker): hook up audio to tracker, we have liftoff
audio still a bit crackly; should probably decouple actual row ticking and rendering of audio (but how does that work with tempo ops?) sequencer goroutine is a bit weird, too, should rethink
This commit is contained in:
parent
175bbb7743
commit
5e45e4f1f4
@ -4,17 +4,24 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
|
"github.com/vsariola/sointu/go4k/audio/oto"
|
||||||
"github.com/vsariola/sointu/go4k/tracker"
|
"github.com/vsariola/sointu/go4k/tracker"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
plr, err := oto.NewPlayer()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer plr.Close()
|
||||||
go func() {
|
go func() {
|
||||||
w := app.NewWindow(
|
w := app.NewWindow(
|
||||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||||
app.Title("Sointu Tracker"),
|
app.Title("Sointu Tracker"),
|
||||||
)
|
)
|
||||||
if err := tracker.New().Run(w); err != nil {
|
if err := tracker.New(plr).Run(w); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,9 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
|
|||||||
switch e.Name {
|
switch e.Name {
|
||||||
case key.NameEscape:
|
case key.NameEscape:
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
case "Space":
|
||||||
|
t.TogglePlay()
|
||||||
|
return true
|
||||||
case key.NameUpArrow:
|
case key.NameUpArrow:
|
||||||
t.CursorRow = (t.CursorRow + t.song.PatternRows() - 1) % t.song.PatternRows()
|
t.CursorRow = (t.CursorRow + t.song.PatternRows() - 1) % t.song.PatternRows()
|
||||||
return true
|
return true
|
||||||
|
@ -19,6 +19,7 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
|
|||||||
t.ActiveTrack == i,
|
t.ActiveTrack == i,
|
||||||
t.CursorRow,
|
t.CursorRow,
|
||||||
t.CursorColumn,
|
t.CursorColumn,
|
||||||
|
int(t.PlayRow),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||||
|
@ -13,6 +13,8 @@ func (t *Tracker) Run(w *app.Window) error {
|
|||||||
var ops op.Ops
|
var ops op.Ops
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
case <-t.ticked:
|
||||||
|
w.Invalidate()
|
||||||
case e := <-w.Events():
|
case e := <-w.Events():
|
||||||
switch e := e.(type) {
|
switch e := e.(type) {
|
||||||
case system.DestroyEvent:
|
case system.DestroyEvent:
|
||||||
|
87
go4k/tracker/sequencer.go
Normal file
87
go4k/tracker/sequencer.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Tracker) TogglePlay() {
|
||||||
|
t.Playing = !t.Playing
|
||||||
|
t.setPlaying <- t.Playing
|
||||||
|
}
|
||||||
|
|
||||||
|
// sequencerLoop is the main goroutine that handles the playing logic
|
||||||
|
func (t *Tracker) sequencerLoop() {
|
||||||
|
playing := false
|
||||||
|
rowTime := (time.Second * 60) / time.Duration(4*t.song.BPM)
|
||||||
|
tick := make(<-chan time.Time)
|
||||||
|
curVoices := make([]int, len(t.song.Tracks))
|
||||||
|
for i := range curVoices {
|
||||||
|
curVoices[i] = t.song.FirstTrackVoice(i)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tick:
|
||||||
|
next := time.Now().Add(rowTime)
|
||||||
|
pattern := atomic.LoadInt32(&t.PlayPattern)
|
||||||
|
row := atomic.LoadInt32(&t.PlayRow)
|
||||||
|
if int(row+1) == t.song.PatternRows() {
|
||||||
|
if int(pattern+1) == t.song.SequenceLength() {
|
||||||
|
atomic.StoreInt32(&t.PlayPattern, 0)
|
||||||
|
} else {
|
||||||
|
atomic.AddInt32(&t.PlayPattern, 1)
|
||||||
|
}
|
||||||
|
atomic.StoreInt32(&t.PlayRow, 0)
|
||||||
|
} else {
|
||||||
|
atomic.AddInt32(&t.PlayRow, 1)
|
||||||
|
}
|
||||||
|
if playing {
|
||||||
|
tick = time.After(next.Sub(time.Now()))
|
||||||
|
}
|
||||||
|
t.playRow(curVoices)
|
||||||
|
t.ticked <- struct{}{}
|
||||||
|
// TODO: maybe refactor the controls to be nicer, somehow?
|
||||||
|
case rowJump := <-t.rowJump:
|
||||||
|
atomic.StoreInt32(&t.PlayRow, int32(rowJump))
|
||||||
|
case patternJump := <-t.patternJump:
|
||||||
|
atomic.StoreInt32(&t.PlayPattern, int32(patternJump))
|
||||||
|
case playState := <-t.setPlaying:
|
||||||
|
playing = playState
|
||||||
|
if playing {
|
||||||
|
t.playBuffer = make([]float32, t.song.SamplesPerRow())
|
||||||
|
tick = time.After(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// playRow renders and writes the current row
|
||||||
|
func (t *Tracker) playRow(curVoices []int) {
|
||||||
|
pattern := atomic.LoadInt32(&t.PlayPattern)
|
||||||
|
row := atomic.LoadInt32(&t.PlayRow)
|
||||||
|
for i, trk := range t.song.Tracks {
|
||||||
|
patternIndex := trk.Sequence[pattern]
|
||||||
|
note := t.song.Patterns[patternIndex][row]
|
||||||
|
if note == 1 { // anything but hold causes an action.
|
||||||
|
continue // TODO: can hold be actually something else than 1?
|
||||||
|
}
|
||||||
|
t.synth.Release(curVoices[i])
|
||||||
|
if note > 1 {
|
||||||
|
curVoices[i]++
|
||||||
|
first := t.song.FirstTrackVoice(i)
|
||||||
|
if curVoices[i] >= first+trk.NumVoices {
|
||||||
|
curVoices[i] = first
|
||||||
|
}
|
||||||
|
t.synth.Trigger(curVoices[i], note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buff := make([]float32, t.song.SamplesPerRow()*2)
|
||||||
|
rendered, timeAdvanced, _ := t.synth.Render(buff, t.song.SamplesPerRow())
|
||||||
|
err := t.player.Play(buff)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("error playing: %w", err)
|
||||||
|
} else if timeAdvanced != t.song.SamplesPerRow() {
|
||||||
|
fmt.Println("rendered only", rendered, "/", timeAdvanced, "expected", t.song.SamplesPerRow())
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ var dark = color.RGBA{R: 24, G: 40, B: 44, 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}
|
||||||
var black = color.RGBA{R: 0, G: 0, B: 0, A: 255}
|
var black = color.RGBA{R: 0, G: 0, B: 0, A: 255}
|
||||||
var yellow = color.RGBA{R: 255, G: 255, B: 130, A: 255}
|
var yellow = color.RGBA{R: 255, G: 255, B: 130, A: 255}
|
||||||
|
var red = color.RGBA{R: 255, G: 0, B: 0, A: 255}
|
||||||
|
|
||||||
var panelColor = neutral
|
var panelColor = neutral
|
||||||
var panelShadeColor = dark
|
var panelShadeColor = dark
|
||||||
@ -31,3 +32,4 @@ var trackerFont = fontCollection[6].Font
|
|||||||
var trackerFontSize = unit.Px(16)
|
var trackerFontSize = unit.Px(16)
|
||||||
var trackerTextColor = white
|
var trackerTextColor = white
|
||||||
var trackerActiveTextColor = yellow
|
var trackerActiveTextColor = yellow
|
||||||
|
var trackerPlayColor = red
|
||||||
|
@ -15,7 +15,7 @@ import (
|
|||||||
const trackRowHeight = 16
|
const trackRowHeight = 16
|
||||||
const trackWidth = 100
|
const trackWidth = 100
|
||||||
|
|
||||||
func (t *Tracker) layoutTrack(notes []byte, active bool, cursorRow, cursorCol int) layout.Widget {
|
func (t *Tracker) layoutTrack(notes []byte, active bool, cursorRow, cursorCol, playingRow int) layout.Widget {
|
||||||
return func(gtx layout.Context) layout.Dimensions {
|
return func(gtx layout.Context) layout.Dimensions {
|
||||||
gtx.Constraints.Min.X = trackWidth
|
gtx.Constraints.Min.X = trackWidth
|
||||||
gtx.Constraints.Max.X = trackWidth
|
gtx.Constraints.Max.X = trackWidth
|
||||||
@ -45,6 +45,9 @@ func (t *Tracker) layoutTrack(notes []byte, active bool, cursorRow, cursorCol in
|
|||||||
}
|
}
|
||||||
op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorRow))).Add(gtx.Ops)
|
op.Offset(f32.Pt(0, (-1*trackRowHeight)*float32(cursorRow))).Add(gtx.Ops)
|
||||||
for i, c := range notes {
|
for i, c := range notes {
|
||||||
|
if i == playingRow {
|
||||||
|
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(trackWidth, trackRowHeight)}.Op())
|
||||||
|
}
|
||||||
if i == cursorRow {
|
if i == cursorRow {
|
||||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
package tracker
|
package tracker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"gioui.org/widget"
|
"gioui.org/widget"
|
||||||
"github.com/vsariola/sointu/go4k"
|
"github.com/vsariola/sointu/go4k"
|
||||||
|
"github.com/vsariola/sointu/go4k/audio"
|
||||||
|
"github.com/vsariola/sointu/go4k/bridge"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Tracker struct {
|
type Tracker struct {
|
||||||
@ -11,14 +14,47 @@ type Tracker struct {
|
|||||||
CursorRow int
|
CursorRow int
|
||||||
CursorColumn int
|
CursorColumn int
|
||||||
DisplayPattern int
|
DisplayPattern int
|
||||||
|
PlayPattern int32
|
||||||
|
PlayRow int32
|
||||||
ActiveTrack int
|
ActiveTrack int
|
||||||
CurrentOctave byte
|
CurrentOctave byte
|
||||||
|
Playing bool
|
||||||
|
ticked chan struct{}
|
||||||
|
setPlaying chan bool
|
||||||
|
rowJump chan int
|
||||||
|
patternJump chan int
|
||||||
|
player audio.Player
|
||||||
|
synth go4k.Synth
|
||||||
|
playBuffer []float32
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Tracker {
|
func (t *Tracker) LoadSong(song go4k.Song) error {
|
||||||
return &Tracker{
|
if err := song.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("invalid song: %w", err)
|
||||||
|
}
|
||||||
|
t.song = song
|
||||||
|
if synth, err := bridge.Synth(song.Patch); err != nil {
|
||||||
|
fmt.Println("error loading synth: %v", err)
|
||||||
|
t.synth = nil
|
||||||
|
} else {
|
||||||
|
t.synth = synth
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(player audio.Player) *Tracker {
|
||||||
|
t := &Tracker{
|
||||||
QuitButton: new(widget.Clickable),
|
QuitButton: new(widget.Clickable),
|
||||||
CurrentOctave: 4,
|
CurrentOctave: 4,
|
||||||
song: defaultSong,
|
player: player,
|
||||||
|
setPlaying: make(chan bool),
|
||||||
|
rowJump: make(chan int),
|
||||||
|
patternJump: make(chan int),
|
||||||
|
ticked: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
go t.sequencerLoop()
|
||||||
|
if err := t.LoadSong(defaultSong); err != nil {
|
||||||
|
panic(fmt.Errorf("cannot load default song: %w", err))
|
||||||
|
}
|
||||||
|
return t
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user