From b4705c941fa80359d39b5b7de810c9e898105d73 Mon Sep 17 00:00:00 2001 From: vsariola <5684185+vsariola@users.noreply.github.com> Date: Sun, 11 Apr 2021 17:51:34 +0300 Subject: [PATCH] feat(tracker, gioui): add visual indicators to show which instruments are playing Closes #44 --- tracker/gioui/instruments.go | 21 ++++++++++++++++++-- tracker/player.go | 38 +++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/tracker/gioui/instruments.go b/tracker/gioui/instruments.go index f3ddf44..9ef50c9 100644 --- a/tracker/gioui/instruments.go +++ b/tracker/gioui/instruments.go @@ -3,6 +3,8 @@ package gioui import ( "fmt" "image" + "image/color" + "math" "strconv" "time" @@ -129,6 +131,21 @@ func (t *Tracker) layoutInstrumentNames(gtx C) D { grabhandle.Text = ":::" } label := func(gtx C) D { + c := 0.0 + voice := t.Song().Patch.FirstVoiceForInstrument(i) + for j := 0; j < t.Song().Patch[i].NumVoices; j++ { + released, event := t.player.VoiceState(voice) + vc := math.Exp(-float64(event)/15000) * .5 + if !released { + vc += .5 + } + if c < vc { + c = vc + } + voice++ + } + k := byte(255 - c*127) + color := color.NRGBA{R: 255, G: k, B: 255, A: 255} if i == t.InstrIndex() { for _, ev := range t.InstrumentNameEditor.Events() { _, ok := ev.(widget.SubmitEvent) @@ -141,7 +158,7 @@ func (t *Tracker) layoutInstrumentNames(gtx C) D { t.InstrumentNameEditor.SetText(n) } editor := material.Editor(t.Theme, t.InstrumentNameEditor, "Instr") - editor.Color = instrumentNameColor + editor.Color = color editor.HintColor = instrumentNameHintColor editor.TextSize = unit.Dp(12) dims := layout.Center.Layout(gtx, editor.Layout) @@ -152,7 +169,7 @@ func (t *Tracker) layoutInstrumentNames(gtx C) D { if text == "" { text = "Instr" } - labelStyle := LabelStyle{Text: text, ShadeColor: black, Color: white, FontSize: unit.Sp(12)} + labelStyle := LabelStyle{Text: text, ShadeColor: black, Color: color, FontSize: unit.Sp(12)} return layout.Center.Layout(gtx, labelStyle.Layout) } return layout.Inset{Left: unit.Dp(6), Right: unit.Dp(6)}.Layout(gtx, func(gtx C) D { diff --git a/tracker/player.go b/tracker/player.go index 192b1a2..977a9cc 100644 --- a/tracker/player.go +++ b/tracker/player.go @@ -13,12 +13,13 @@ type Player struct { playCmds chan uint64 - mutex sync.Mutex - runningID uint32 - voiceNoteID []uint32 - voiceReleased []bool - synth sointu.Synth - patch sointu.Patch + mutex sync.Mutex + runningID uint32 + voiceNoteID []uint32 + voiceReleased []int32 + synth sointu.Synth + patch sointu.Patch + samplesSinceEvent []int32 synthNotNil int32 } @@ -66,6 +67,13 @@ func (p *Player) Enabled() bool { return atomic.LoadInt32(&p.synthNotNil) == 1 } +func (p *Player) VoiceState(voice int) (bool, int) { + if voice >= len(p.samplesSinceEvent) || voice >= len(p.voiceReleased) { + return true, math.MaxInt32 + } + return atomic.LoadInt32(&p.voiceReleased[voice]) == 1, int(atomic.LoadInt32(&p.samplesSinceEvent[voice])) +} + func NewPlayer(service sointu.SynthService, closer <-chan struct{}, patchs <-chan sointu.Patch, scores <-chan sointu.Score, samplesPerRows <-chan int, posChanged chan<- struct{}, syncOutput chan<- []float32, outputs ...chan<- []float32) *Player { p := &Player{playCmds: make(chan uint64, 16)} go func() { @@ -189,6 +197,9 @@ func NewPlayer(service sointu.SynthService, closer <-chan struct{}, patchs <-cha default: } } + for i := range p.samplesSinceEvent { + atomic.AddInt32(&p.samplesSinceEvent[i], int32(timeAdvanced)) + } for _, o := range outputs { o <- buffer[:rendered*2] } @@ -243,7 +254,10 @@ func (p *Player) trigger(voiceStart, voiceEnd int, note byte) uint32 { oldestVoice := 0 for i := voiceStart; i < voiceEnd; i++ { for len(p.voiceReleased) <= i { - p.voiceReleased = append(p.voiceReleased, true) + p.voiceReleased = append(p.voiceReleased, 1) + } + for len(p.samplesSinceEvent) <= i { + p.samplesSinceEvent = append(p.samplesSinceEvent, 0) } for len(p.voiceNoteID) <= i { p.voiceNoteID = append(p.voiceNoteID, 0) @@ -253,7 +267,7 @@ func (p *Player) trigger(voiceStart, voiceEnd int, note byte) uint32 { // case two voices are both playing or or both are released, we prefer // the older one id := p.voiceNoteID[i] - isReleased := p.voiceReleased[i] + isReleased := atomic.LoadInt32(&p.voiceReleased[i]) == 1 if id < oldestID && (oldestReleased == isReleased) || (!oldestReleased && isReleased) { oldestVoice = i oldestID = id @@ -261,7 +275,8 @@ func (p *Player) trigger(voiceStart, voiceEnd int, note byte) uint32 { } } p.voiceNoteID[oldestVoice] = newID - p.voiceReleased[oldestVoice] = false + atomic.StoreInt32(&p.voiceReleased[oldestVoice], 0) + atomic.StoreInt32(&p.samplesSinceEvent[oldestVoice], 0) if p.synth != nil { p.synth.Trigger(oldestVoice, note) } @@ -273,8 +288,9 @@ func (p *Player) release(ID uint32) { return } for i := 0; i < len(p.voiceNoteID); i++ { - if p.voiceNoteID[i] == ID && !p.voiceReleased[i] { - p.voiceReleased[i] = true + if p.voiceNoteID[i] == ID && atomic.LoadInt32(&p.voiceReleased[i]) != 1 { + atomic.StoreInt32(&p.voiceReleased[i], 1) + atomic.StoreInt32(&p.samplesSinceEvent[i], 0) p.synth.Release(i) return }