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"
|
||||
"gioui.org/app"
|
||||
"gioui.org/unit"
|
||||
"github.com/vsariola/sointu/go4k/audio/oto"
|
||||
"github.com/vsariola/sointu/go4k/tracker"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plr, err := oto.NewPlayer()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer plr.Close()
|
||||
go func() {
|
||||
w := app.NewWindow(
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
app.Title("Sointu Tracker"),
|
||||
)
|
||||
if err := tracker.New().Run(w); err != nil {
|
||||
if err := tracker.New(plr).Run(w); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
@ -50,6 +50,9 @@ func (t *Tracker) KeyEvent(e key.Event) bool {
|
||||
switch e.Name {
|
||||
case key.NameEscape:
|
||||
os.Exit(0)
|
||||
case "Space":
|
||||
t.TogglePlay()
|
||||
return true
|
||||
case key.NameUpArrow:
|
||||
t.CursorRow = (t.CursorRow + t.song.PatternRows() - 1) % t.song.PatternRows()
|
||||
return true
|
||||
|
@ -19,6 +19,7 @@ func (t *Tracker) layoutTracker(gtx layout.Context) layout.Dimensions {
|
||||
t.ActiveTrack == i,
|
||||
t.CursorRow,
|
||||
t.CursorColumn,
|
||||
int(t.PlayRow),
|
||||
)))
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
|
@ -13,6 +13,8 @@ func (t *Tracker) Run(w *app.Window) error {
|
||||
var ops op.Ops
|
||||
for {
|
||||
select {
|
||||
case <-t.ticked:
|
||||
w.Invalidate()
|
||||
case e := <-w.Events():
|
||||
switch e := e.(type) {
|
||||
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 black = color.RGBA{R: 0, G: 0, B: 0, 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 panelShadeColor = dark
|
||||
@ -31,3 +32,4 @@ var trackerFont = fontCollection[6].Font
|
||||
var trackerFontSize = unit.Px(16)
|
||||
var trackerTextColor = white
|
||||
var trackerActiveTextColor = yellow
|
||||
var trackerPlayColor = red
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
const trackRowHeight = 16
|
||||
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 {
|
||||
gtx.Constraints.Min.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)
|
||||
for i, c := range notes {
|
||||
if i == playingRow {
|
||||
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(trackWidth, trackRowHeight)}.Op())
|
||||
}
|
||||
if i == cursorRow {
|
||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||
} else {
|
||||
|
@ -1,8 +1,11 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gioui.org/widget"
|
||||
"github.com/vsariola/sointu/go4k"
|
||||
"github.com/vsariola/sointu/go4k/audio"
|
||||
"github.com/vsariola/sointu/go4k/bridge"
|
||||
)
|
||||
|
||||
type Tracker struct {
|
||||
@ -11,14 +14,47 @@ type Tracker struct {
|
||||
CursorRow int
|
||||
CursorColumn int
|
||||
DisplayPattern int
|
||||
PlayPattern int32
|
||||
PlayRow int32
|
||||
ActiveTrack int
|
||||
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 {
|
||||
return &Tracker{
|
||||
func (t *Tracker) LoadSong(song go4k.Song) error {
|
||||
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),
|
||||
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