diff --git a/tracker/sequencer.go b/tracker/sequencer.go index 01efa9f..47a0f95 100644 --- a/tracker/sequencer.go +++ b/tracker/sequencer.go @@ -47,7 +47,7 @@ type noteID struct { id uint32 } -func NewSequencer(bufferSize int, service sointu.SynthService, context sointu.AudioContext, iterator func([]RowNote) []RowNote) *Sequencer { +func NewSequencer(bufferSize int, service sointu.SynthService, context sointu.AudioContext, callBack func([]float32), iterator func([]RowNote) []RowNote) *Sequencer { ret := &Sequencer{ closer: make(chan struct{}), setPatch: make(chan sointu.Patch, 32), @@ -60,11 +60,11 @@ func NewSequencer(bufferSize int, service sointu.SynthService, context sointu.Au // the iterator is a bit unconventional in the sense that it might return // false to indicate that there is no row available, but might still return // true in future attempts if new rows become available. - go ret.loop(bufferSize, service, context, iterator) + go ret.loop(bufferSize, service, context, callBack, iterator) return ret } -func (s *Sequencer) loop(bufferSize int, service sointu.SynthService, context sointu.AudioContext, iterator func([]RowNote) []RowNote) { +func (s *Sequencer) loop(bufferSize int, service sointu.SynthService, context sointu.AudioContext, callBack func([]float32), iterator func([]RowNote) []RowNote) { buffer := make([]float32, bufferSize) renderTries := 0 audioOut := context.Output() @@ -141,6 +141,7 @@ func (s *Sequencer) loop(bufferSize int, service sointu.SynthService, context so } } rendered, timeAdvanced, err := s.synth.Render(buffer, renderTime) + callBack(buffer) if err != nil { s.Disable() break diff --git a/tracker/songpanel.go b/tracker/songpanel.go index 70afcb2..707353b 100644 --- a/tracker/songpanel.go +++ b/tracker/songpanel.go @@ -192,5 +192,6 @@ func (t *Tracker) layoutSongOptions(gtx C) D { gtx.Constraints.Min = image.Pt(0, 0) return panicBtnStyle.Layout(gtx) }), + layout.Rigid(t.VuMeter.Layout), ) } diff --git a/tracker/tracker.go b/tracker/tracker.go index 685780c..ec2bdf5 100644 --- a/tracker/tracker.go +++ b/tracker/tracker.go @@ -71,6 +71,7 @@ type Tracker struct { InstrumentDragList *DragList TrackHexCheckBoxes []*widget.Bool TrackShowHex []bool + VuMeter VuMeter TopHorizontalSplit *Split BottomHorizontalSplit *Split VerticalSplit *Split @@ -695,10 +696,23 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr t.TopHorizontalSplit.Ratio = -.6 t.Theme.Palette.Fg = primaryColor t.Theme.Palette.ContrastFg = black + t.EditMode = EditTracks + t.Step.Value = 1 + t.VuMeter.FallOff = 2e-8 + t.VuMeter.RangeDb = 80 + t.VuMeter.Decay = 1e-3 for range allUnits { t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable)) } - t.sequencer = NewSequencer(2048, synthService, audioContext, func(row []RowNote) []RowNote { + callBack := func(buf []float32) { + t.VuMeter.Update(buf) + select { + case t.refresh <- struct{}{}: + default: + // message dropped, there's already a tick queued, so no need to queue extra + } + } + t.sequencer = NewSequencer(2048, synthService, audioContext, callBack, func(row []RowNote) []RowNote { t.playRowPatMutex.Lock() if !t.Playing { t.playRowPatMutex.Unlock() diff --git a/tracker/vumeter.go b/tracker/vumeter.go new file mode 100644 index 0000000..99eb8b1 --- /dev/null +++ b/tracker/vumeter.go @@ -0,0 +1,78 @@ +package tracker + +import ( + "image" + "math" + + "gioui.org/f32" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" +) + +type VuMeter struct { + avg [2]float32 + max [2]float32 + speed [2]float32 + FallOff float32 + Decay float32 + RangeDb float32 +} + +func (v *VuMeter) Update(buffer []float32) { + for j := 0; j < 2; j++ { + for i := 0; i < len(buffer); i += 2 { + sample2 := buffer[i+j] * buffer[i+j] + db := float32(10*math.Log10(float64(sample2))) + v.RangeDb + v.speed[j] += v.FallOff + v.max[j] -= v.speed[j] + if v.max[j] < 0 { + v.max[j] = 0 + } + if v.max[j] < db { + v.max[j] = db + v.speed[j] = 0 + } + v.avg[j] += (sample2 - v.avg[j]) * v.Decay + if math.IsNaN(float64(v.avg[j])) { + v.avg[j] = 0 + } + } + } +} + +func (v *VuMeter) Reset() { + v.avg = [2]float32{} + v.max = [2]float32{} +} + +func (v *VuMeter) Layout(gtx C) D { + defer op.Save(gtx.Ops).Load() + gtx.Constraints.Max.Y = gtx.Px(unit.Dp(12)) + height := gtx.Px(unit.Dp(6)) + for j := 0; j < 2; j++ { + value := float32(10*math.Log10(float64(v.avg[j]))) + v.RangeDb + if value > 0 { + x := int(value/v.RangeDb*float32(gtx.Constraints.Max.X) + 0.5) + if x > gtx.Constraints.Max.X { + x = gtx.Constraints.Max.X + } + paint.FillShape(gtx.Ops, mediumEmphasisTextColor, clip.Rect(image.Rect(0, 0, x, height)).Op()) + } + valueMax := v.max[j] + if valueMax > 0 { + color := white + if valueMax >= v.RangeDb { + color = errorColor + } + x := int(valueMax/v.RangeDb*float32(gtx.Constraints.Max.X) + 0.5) + if x > gtx.Constraints.Max.X { + x = gtx.Constraints.Max.X + } + paint.FillShape(gtx.Ops, color, clip.Rect(image.Rect(x-1, 0, x, height)).Op()) + } + op.Offset(f32.Pt(0, float32(height))).Add(gtx.Ops) + } + return D{Size: gtx.Constraints.Max} +}