mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
feat(tracker): add a rudimentary VU-meter to show master volume, peaks & clipping
Closes #16
This commit is contained in:
parent
962d0f1152
commit
088bbc6c58
@ -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
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
@ -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
78
tracker/vumeter.go
Normal 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}
|
||||
}
|
Loading…
Reference in New Issue
Block a user