feat(tracker): add a rudimentary VU-meter to show master volume, peaks & clipping

Closes #16
This commit is contained in:
vsariola 2021-02-16 12:23:18 +02:00
parent 962d0f1152
commit 088bbc6c58
4 changed files with 98 additions and 4 deletions

View File

@ -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

View File

@ -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),
)
}

View File

@ -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()

78
tracker/vumeter.go Normal file
View File

@ -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}
}