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
|
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{
|
ret := &Sequencer{
|
||||||
closer: make(chan struct{}),
|
closer: make(chan struct{}),
|
||||||
setPatch: make(chan sointu.Patch, 32),
|
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
|
// 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
|
// false to indicate that there is no row available, but might still return
|
||||||
// true in future attempts if new rows become available.
|
// 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
|
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)
|
buffer := make([]float32, bufferSize)
|
||||||
renderTries := 0
|
renderTries := 0
|
||||||
audioOut := context.Output()
|
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)
|
rendered, timeAdvanced, err := s.synth.Render(buffer, renderTime)
|
||||||
|
callBack(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Disable()
|
s.Disable()
|
||||||
break
|
break
|
||||||
|
@ -192,5 +192,6 @@ func (t *Tracker) layoutSongOptions(gtx C) D {
|
|||||||
gtx.Constraints.Min = image.Pt(0, 0)
|
gtx.Constraints.Min = image.Pt(0, 0)
|
||||||
return panicBtnStyle.Layout(gtx)
|
return panicBtnStyle.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
|
layout.Rigid(t.VuMeter.Layout),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,7 @@ type Tracker struct {
|
|||||||
InstrumentDragList *DragList
|
InstrumentDragList *DragList
|
||||||
TrackHexCheckBoxes []*widget.Bool
|
TrackHexCheckBoxes []*widget.Bool
|
||||||
TrackShowHex []bool
|
TrackShowHex []bool
|
||||||
|
VuMeter VuMeter
|
||||||
TopHorizontalSplit *Split
|
TopHorizontalSplit *Split
|
||||||
BottomHorizontalSplit *Split
|
BottomHorizontalSplit *Split
|
||||||
VerticalSplit *Split
|
VerticalSplit *Split
|
||||||
@ -695,10 +696,23 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr
|
|||||||
t.TopHorizontalSplit.Ratio = -.6
|
t.TopHorizontalSplit.Ratio = -.6
|
||||||
t.Theme.Palette.Fg = primaryColor
|
t.Theme.Palette.Fg = primaryColor
|
||||||
t.Theme.Palette.ContrastFg = black
|
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 {
|
for range allUnits {
|
||||||
t.ChooseUnitTypeBtns = append(t.ChooseUnitTypeBtns, new(widget.Clickable))
|
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()
|
t.playRowPatMutex.Lock()
|
||||||
if !t.Playing {
|
if !t.Playing {
|
||||||
t.playRowPatMutex.Unlock()
|
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…
x
Reference in New Issue
Block a user