diff --git a/tracker/gioui/oscilloscope.go b/tracker/gioui/oscilloscope.go index 9d66900..d44ba89 100644 --- a/tracker/gioui/oscilloscope.go +++ b/tracker/gioui/oscilloscope.go @@ -44,6 +44,7 @@ func Scope(th *Theme, m *tracker.ScopeModel, st *OscilloscopeState) Oscilloscope } func (s *Oscilloscope) Layout(gtx C) D { + t := TrackerFromContext(gtx) leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout @@ -58,11 +59,11 @@ func (s *Oscilloscope) Layout(gtx C) D { w := s.Model.Waveform() cx := float32(w.Cursor) / float32(len(w.Buffer)) - data := func(chn int, xr plotRange) (yr plotRange) { + data := func(chn int, xr plotRange) (yr plotRange, ok bool) { x1 := max(int(xr.a*float32(len(w.Buffer))), 0) x2 := min(int(xr.b*float32(len(w.Buffer))), len(w.Buffer)-1) if x1 > x2 { - return plotRange{1, 0} + return plotRange{}, false } y1 := float32(math.Inf(-1)) y2 := float32(math.Inf(+1)) @@ -71,18 +72,31 @@ func (s *Oscilloscope) Layout(gtx C) D { y1 = max(y1, sample) y2 = min(y2, sample) } - return plotRange{-y1, -y2} + return plotRange{-y1, -y2}, true } - xticks := func(r plotRange, yield func(pos float32, label string)) { - l := s.Model.LengthInBeats().Value() + rpb := max(t.Model.RowsPerBeat().Value(), 1) + xticks := func(r plotRange, count int, yield func(pos float32, label string)) { + l := s.Model.LengthInBeats().Value() * rpb a := max(int(math.Ceil(float64(r.a*float32(l)))), 0) b := min(int(math.Floor(float64(r.b*float32(l)))), l) - for i := a; i <= b; i++ { - yield(float32(i)/float32(l), strconv.Itoa(i)) + step := 1 + n := rpb + for (b-a+1)/step > count { + step *= n + n = 2 + } + a = (a / step) * step + for i := a; i <= b; i += step { + if i%rpb == 0 { + beat := i / rpb + yield(float32(i)/float32(l), strconv.Itoa(beat)) + } else { + yield(float32(i)/float32(l), "") + } } } - yticks := func(r plotRange, yield func(pos float32, label string)) { + yticks := func(r plotRange, count int, yield func(pos float32, label string)) { yield(-1, "") yield(1, "") } diff --git a/tracker/gioui/plot.go b/tracker/gioui/plot.go index 138296e..e6b757e 100644 --- a/tracker/gioui/plot.go +++ b/tracker/gioui/plot.go @@ -11,6 +11,7 @@ import ( "gioui.org/op" "gioui.org/op/clip" "gioui.org/op/paint" + "gioui.org/unit" ) type ( @@ -25,14 +26,15 @@ type ( } PlotStyle struct { - CurveColors [2]color.NRGBA `yaml:",flow"` + CurveColors [3]color.NRGBA `yaml:",flow"` LimitColor color.NRGBA `yaml:",flow"` CursorColor color.NRGBA `yaml:",flow"` Ticks LabelStyle + DpPerTick unit.Dp } - PlotDataFunc func(chn int, xr plotRange) (yr plotRange) - PlotTickFunc func(r plotRange, yield func(pos float32, label string)) + PlotDataFunc func(chn int, xr plotRange) (yr plotRange, ok bool) + PlotTickFunc func(r plotRange, num int, yield func(pos float32, label string)) plotRange struct{ a, b float32 } plotRel float32 plotPx int @@ -61,8 +63,8 @@ func (p *Plot) Layout(gtx C, data PlotDataFunc, xticks, yticks PlotTickFunc, cur ylim := p.ylim() // draw tick marks - - xticks(xlim, func(x float32, txt string) { + numxticks := s.X / gtx.Dp(style.DpPerTick) + xticks(xlim, numxticks, func(x float32, txt string) { paint.ColorOp{Color: style.LimitColor}.Add(gtx.Ops) sx := plotPx(s.X).toScreen(xlim.toRelative(x)) fillRect(gtx, clip.Rect{Min: image.Pt(sx, 0), Max: image.Pt(sx+1, s.Y)}) @@ -70,7 +72,8 @@ func (p *Plot) Layout(gtx C, data PlotDataFunc, xticks, yticks PlotTickFunc, cur Label(t.Theme, &t.Theme.Plot.Ticks, txt).Layout(gtx) }) - yticks(ylim, func(y float32, txt string) { + numyticks := s.Y / gtx.Dp(style.DpPerTick) + yticks(ylim, numyticks, func(y float32, txt string) { paint.ColorOp{Color: style.LimitColor}.Add(gtx.Ops) sy := plotPx(s.Y).toScreen(ylim.toRelative(y)) fillRect(gtx, clip.Rect{Min: image.Pt(0, sy), Max: image.Pt(s.X, sy+1)}) @@ -91,13 +94,13 @@ func (p *Plot) Layout(gtx C, data PlotDataFunc, xticks, yticks PlotTickFunc, cur // left and right is the sample range covered by the pixel left := right right = xlim.fromRelative(plotPx(s.X).fromScreen(sx + 1)) - yr := data(chn, plotRange{left, right}) - if yr.b < yr.a { + yr, ok := data(chn, plotRange{left, right}) + if !ok { continue } y1 := plotPx(s.Y).toScreen(ylim.toRelative(yr.a)) y2 := plotPx(s.Y).toScreen(ylim.toRelative(yr.b)) - fillRect(gtx, clip.Rect{Min: image.Pt(sx, y1), Max: image.Pt(sx+1, y2+1)}) + fillRect(gtx, clip.Rect{Min: image.Pt(sx, min(y1, y2)), Max: image.Pt(sx+1, max(y1, y2)+1)}) } } return D{Size: s} diff --git a/tracker/gioui/specanalyzer.go b/tracker/gioui/specanalyzer.go index 4f049fd..1c3798e 100644 --- a/tracker/gioui/specanalyzer.go +++ b/tracker/gioui/specanalyzer.go @@ -2,6 +2,7 @@ package gioui import ( "math" + "strconv" "gioui.org/layout" "gioui.org/unit" @@ -17,9 +18,11 @@ type ( } ) +const SpectrumDisplayDb = 60 + func NewSpectrumState() *SpectrumState { return &SpectrumState{ - plot: NewPlot(plotRange{0, 1}, plotRange{-1, 0}), + plot: NewPlot(plotRange{-4, 0}, plotRange{SpectrumDisplayDb, 0}), resolutionNumber: NewNumericUpDownState(), smoothingBtn: new(Clickable), chnModeBtn: new(Clickable), @@ -67,15 +70,27 @@ func (s *SpectrumState) Layout(gtx C) D { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Flexed(1, func(gtx C) D { - data := func(chn int, xr plotRange) (yr plotRange) { - xr.a = float32(math.Exp2(float64(xr.a-1) * 8)) - xr.b = float32(math.Exp2(float64(xr.b-1) * 8)) + biquad, biquadok := t.Model.BiquadCoeffs() + data := func(chn int, xr plotRange) (yr plotRange, ok bool) { + if chn == 2 { + if xr.a >= 0 { + return plotRange{}, false + } + ya := math.Log10(float64(biquad.Gain(float32(math.Pi*math.Pow(10, float64(xr.a)))))) * 20 + yb := math.Log10(float64(biquad.Gain(float32(math.Pi*math.Pow(10, float64(xr.b)))))) * 20 + return plotRange{float32(ya) + SpectrumDisplayDb, float32(yb) + SpectrumDisplayDb}, true + } + if chn >= numchns { + return plotRange{}, false + } + xr.a = float32(math.Pow(10, float64(xr.a))) + xr.b = float32(math.Pow(10, float64(xr.b))) w1, f1 := math.Modf(float64(xr.a) * float64(speclen)) w2, f2 := math.Modf(float64(xr.b) * float64(speclen)) x1 := max(int(w1), 0) x2 := min(int(w2), speclen-1) if x1 > x2 { - return plotRange{1, 0} + return plotRange{}, false } y1 := float32(math.Inf(-1)) y2 := float32(math.Inf(+1)) @@ -95,18 +110,18 @@ func (s *SpectrumState) Layout(gtx C) D { y2 = min(y2, sample) } } - y1 = (y1 / 80) + 1 - y2 = (y2 / 80) + 1 - y1 = softplus(y1*10) / 10 - y2 = softplus(y2*10) / 10 + y1 = SpectrumDisplayDb + y1 + y2 = SpectrumDisplayDb + y2 + y1 = softplus(y1/5) * 5 // we "squash" the low volumes so the -Inf dB becomes -SpectrumDisplayDb + y2 = softplus(y2/5) * 5 - return plotRange{-y1, -y2} + return plotRange{y1, y2}, true } - type pair struct { - freq float64 - label string - } - xticks := func(r plotRange, yield func(pos float32, label string)) { + xticks := func(r plotRange, count int, yield func(pos float32, label string)) { + type pair struct { + freq float64 + label string + } for _, p := range []pair{ {freq: 10, label: "10 Hz"}, {freq: 20, label: "20 Hz"}, @@ -120,17 +135,32 @@ func (s *SpectrumState) Layout(gtx C) D { {freq: 1e4, label: "10 kHz"}, {freq: 2e4, label: "20 kHz"}, } { - x := float32(math.Log2((p.freq/22050))/8 + 1) + x := float32(math.Log10(p.freq / 22050)) if x >= r.a && x <= r.b { yield(x, p.label) } } } - yticks := func(r plotRange, yield func(pos float32, label string)) { - yield(-1, "0 dB") - yield(0, "-Inf dB") + yticks := func(r plotRange, count int, yield func(pos float32, label string)) { + step := 3 + var start, end int + for { + start = int(math.Ceil(float64(r.b-SpectrumDisplayDb) / float64(step))) + end = int(math.Floor(float64(r.a-SpectrumDisplayDb) / float64(step))) + step *= 2 + if end-start+1 <= count*4 { // we use 4x density for the y-lines in the spectrum + break + } + } + for i := start; i <= end; i++ { + yield(float32(i*step)+SpectrumDisplayDb, strconv.Itoa(i*step)) + } } - return s.plot.Layout(gtx, data, xticks, yticks, 0, numchns) + n := numchns + if biquadok { + n = 3 + } + return s.plot.Layout(gtx, data, xticks, yticks, 0, n) }), layout.Rigid(func(gtx C) D { return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, diff --git a/tracker/gioui/theme.yml b/tracker/gioui/theme.yml index 78ce13c..d5d5850 100644 --- a/tracker/gioui/theme.yml +++ b/tracker/gioui/theme.yml @@ -84,10 +84,11 @@ iconbutton: size: 24 inset: { top: 6, bottom: 6, left: 6, right: 6 } plot: - curvecolors: [*primarycolor, *secondarycolor] + curvecolors: [*primarycolor, *secondarycolor,*disabled] limitcolor: { r: 255, g: 255, b: 255, a: 8 } cursorcolor: { r: 252, g: 186, b: 3, a: 255 } ticks: { textsize: 12, color: *disabled, maxlines: 1} + dppertick: 50 numericupdown: bgcolor: { r: 255, g: 255, b: 255, a: 3 } textcolor: *fg diff --git a/tracker/spectrum.go b/tracker/spectrum.go index e44324e..2017ba8 100644 --- a/tracker/spectrum.go +++ b/tracker/spectrum.go @@ -34,6 +34,11 @@ type ( tmpC []complex128 // temporary buffer for FFT tmp1, tmp2 []float32 // temporary buffers for processing } + + BiquadCoeffs struct { + b0, b1, b2 float32 + a0, a1, a2 float32 + } ) const ( @@ -72,6 +77,60 @@ func NewSpecAnalyzer(broker *Broker) *SpecAnalyzer { return ret } +func (m *Model) BiquadCoeffs() (coeffs BiquadCoeffs, ok bool) { + i := m.d.InstrIndex + u := m.d.UnitIndex + if i < 0 || i >= len(m.d.Song.Patch) || u < 0 || u >= len(m.d.Song.Patch[i].Units) { + return BiquadCoeffs{}, false + } + switch m.d.Song.Patch[i].Units[u].Type { + case "filter": + p := m.d.Song.Patch[i].Units[u].Parameters + f := float32(p["frequency"]) / 128 + res := float32(p["resonance"]) / 128 + f2 := f * f + g := f2 / (2 - f2) + a1 := 2 * (g*g - 1) + a2 := (1 - g*(g-res)) + var b0, b1, b2 float32 + if p["low"] == 1 { + b0 += g * g + b1 += 2 * g * g + b2 += g * g + } + b0 += float32(p["high"]) + b1 += -2 * float32(p["high"]) + b2 += float32(p["high"]) + b0 += g * float32(p["band"]) + b2 += -g * float32(p["band"]) + return BiquadCoeffs{a0: 1, a1: a1, a2: a2, b0: b0, b1: b1, b2: b2}, true + case "belleq": + f := float32(m.d.Song.Patch[i].Units[u].Parameters["frequency"]) / 128 + band := float32(m.d.Song.Patch[i].Units[u].Parameters["bandwidth"]) / 128 + gain := float32(m.d.Song.Patch[i].Units[u].Parameters["gain"]) / 128 + omega0 := 2 * f * f + alpha := float32(math.Sin(float64(omega0))) * 2 * band + A := float32(math.Pow(2, float64(gain-.5)*6.643856189774724)) + u, v := alpha*A, alpha/A + return BiquadCoeffs{ + b0: 1 + u, + b1: -2 * float32(math.Cos(float64(omega0))), + b2: 1 - u, + a0: 1 + v, + a1: -2 * float32(math.Cos(float64(omega0))), + a2: 1 - v, + }, true + default: + return BiquadCoeffs{}, false + } +} + +func (c *BiquadCoeffs) Gain(omega float32) float32 { + e := cmplx.Rect(1, -float64(omega)) + return float32(cmplx.Abs((complex(float64(c.b0), 0) + complex(float64(c.b1), 0)*e + complex(float64(c.b2), 0)*(e*e)) / + (complex(float64(c.a0), 0) + complex(float64(c.a1), 0)*e + complex(float64(c.a2), 0)*e*e))) +} + func (s *SpecAnalyzer) Run() { for { select {