diff --git a/tracker/gioui/oscilloscope.go b/tracker/gioui/oscilloscope.go index d44ba89..0f3917d 100644 --- a/tracker/gioui/oscilloscope.go +++ b/tracker/gioui/oscilloscope.go @@ -27,7 +27,7 @@ type ( func NewOscilloscope(model *tracker.Model) *OscilloscopeState { return &OscilloscopeState{ - plot: NewPlot(plotRange{0, 1}, plotRange{-1, 1}), + plot: NewPlot(plotRange{0, 1}, plotRange{-1, 1}, 0), onceBtn: new(Clickable), wrapBtn: new(Clickable), lengthInBeatsNumber: NewNumericUpDownState(), diff --git a/tracker/gioui/plot.go b/tracker/gioui/plot.go index e6b757e..dbcda5c 100644 --- a/tracker/gioui/plot.go +++ b/tracker/gioui/plot.go @@ -17,6 +17,7 @@ import ( type ( Plot struct { origXlim, origYlim plotRange + fixedYLevel float32 xScale, yScale float32 xOffset float32 @@ -41,10 +42,11 @@ type ( plotLogScale float32 ) -func NewPlot(xlim, ylim plotRange) *Plot { +func NewPlot(xlim, ylim plotRange, fixedYLevel float32) *Plot { return &Plot{ - origXlim: xlim, - origYlim: ylim, + origXlim: xlim, + origYlim: ylim, + fixedYLevel: fixedYLevel, } } @@ -82,9 +84,11 @@ func (p *Plot) Layout(gtx C, data PlotDataFunc, xticks, yticks PlotTickFunc, cur }) // draw cursor - paint.ColorOp{Color: style.CursorColor}.Add(gtx.Ops) - csx := plotPx(s.X).toScreen(xlim.toRelative(cursornx)) - fillRect(gtx, clip.Rect{Min: image.Pt(csx, 0), Max: image.Pt(csx+1, s.Y)}) + if cursornx == cursornx { // check for NaN + paint.ColorOp{Color: style.CursorColor}.Add(gtx.Ops) + csx := plotPx(s.X).toScreen(xlim.toRelative(cursornx)) + fillRect(gtx, clip.Rect{Min: image.Pt(csx, 0), Max: image.Pt(csx+1, s.Y)}) + } // draw curves for chn := range numchns { @@ -119,7 +123,9 @@ func (s plotPx) fromScreen(px int) plotRel { return plotRel(float32(px) / func (s plotPx) fromScreenF32(px float32) plotRel { return plotRel(px / float32(s-1)) } func (o *Plot) xlim() plotRange { return o.origXlim.scale(o.xScale).offset(o.xOffset) } -func (o *Plot) ylim() plotRange { return o.origYlim.scale(o.yScale) } +func (o *Plot) ylim() plotRange { + return o.origYlim.offset(-o.fixedYLevel).scale(o.yScale).offset(o.fixedYLevel) +} func fillRect(gtx C, rect clip.Rect) { stack := rect.Push(gtx.Ops) @@ -164,6 +170,8 @@ func (o *Plot) update(gtx C) { num := o.ylim().fromRelative(plotPx(s.Y).fromScreenF32(e.Position.Y)) den := o.ylim().fromRelative(plotPx(s.Y).fromScreenF32(o.dragStartPoint.Y)) + num -= o.fixedYLevel + den -= o.fixedYLevel if l := math.Abs(float64(num / den)); l > 1e-3 && l < 1e3 { o.yScale -= float32(math.Log(l)) o.yScale = min(max(o.yScale, -1e3), 1e3) diff --git a/tracker/gioui/specanalyzer.go b/tracker/gioui/specanalyzer.go index 1c3798e..917ed2a 100644 --- a/tracker/gioui/specanalyzer.go +++ b/tracker/gioui/specanalyzer.go @@ -1,6 +1,7 @@ package gioui import ( + "fmt" "math" "strconv" @@ -12,19 +13,22 @@ import ( type ( SpectrumState struct { resolutionNumber *NumericUpDownState - smoothingBtn *Clickable + speed *NumericUpDownState chnModeBtn *Clickable plot *Plot } ) -const SpectrumDisplayDb = 60 +const ( + SpectrumDbMin = -60 + SpectrumDbMax = 12 +) func NewSpectrumState() *SpectrumState { return &SpectrumState{ - plot: NewPlot(plotRange{-4, 0}, plotRange{SpectrumDisplayDb, 0}), + plot: NewPlot(plotRange{-4, 0}, plotRange{SpectrumDbMax, SpectrumDbMin}, SpectrumDbMin), resolutionNumber: NewNumericUpDownState(), - smoothingBtn: new(Clickable), + speed: NewNumericUpDownState(), chnModeBtn: new(Clickable), } } @@ -32,32 +36,20 @@ func NewSpectrumState() *SpectrumState { func (s *SpectrumState) Layout(gtx C) D { s.Update(gtx) t := TrackerFromContext(gtx) - leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(24)}.Layout + leftSpacer := layout.Spacer{Width: unit.Dp(6), Height: unit.Dp(36)}.Layout rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout var chnModeTxt string = "???" switch tracker.SpecChnMode(t.Model.SpecAnChannelsInt().Value()) { - case tracker.SpecChnModeCombine: + case tracker.SpecChnModeSum: chnModeTxt = "Sum" case tracker.SpecChnModeSeparate: chnModeTxt = "Separate" - case tracker.SpecChnModeOff: - chnModeTxt = "Off" - } - - var smoothTxt string = "???" - switch tracker.SpecSmoothing(t.Model.SpecAnSmoothing().Value()) { - case tracker.SpecSmoothingSlow: - smoothTxt = "Slow" - case tracker.SpecSmoothingMedium: - smoothTxt = "Medium" - case tracker.SpecSmoothingFast: - smoothTxt = "Fast" } resolution := NumUpDown(t.Model.SpecAnResolution(), t.Theme, s.resolutionNumber, "Resolution") chnModeBtn := Btn(t.Theme, &t.Theme.Button.Filled, s.chnModeBtn, chnModeTxt, "Channel mode") - smoothBtn := Btn(t.Theme, &t.Theme.Button.Filled, s.smoothingBtn, smoothTxt, "Smoothing") + speed := NumUpDown(t.Model.SpecAnSpeed(), t.Theme, s.speed, "Speed") numchns := 0 speclen := len(t.Model.Spectrum()[0]) @@ -78,15 +70,15 @@ func (s *SpectrumState) Layout(gtx C) D { } 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 + return plotRange{float32(ya), float32(yb)}, 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)) + w1, f1 := math.Modf(float64(xr.a)*float64(speclen) - 1) // -1 cause we don't have the DC bin there + w2, f2 := math.Modf(float64(xr.b)*float64(speclen) - 1) // -1 cause we don't have the DC bin there x1 := max(int(w1), 0) x2 := min(int(w2), speclen-1) if x1 > x2 { @@ -110,10 +102,8 @@ func (s *SpectrumState) Layout(gtx C) D { y2 = min(y2, sample) } } - 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 + y1 = softplus((y1-SpectrumDbMin)/5)*5 + SpectrumDbMin // we "squash" the low volumes so the -Inf dB becomes -SpectrumDbMin + y2 = softplus((y2-SpectrumDbMin)/5)*5 + SpectrumDbMin return plotRange{y1, y2}, true } @@ -122,22 +112,23 @@ func (s *SpectrumState) Layout(gtx C) D { freq float64 label string } - for _, p := range []pair{ - {freq: 10, label: "10 Hz"}, - {freq: 20, label: "20 Hz"}, - {freq: 50, label: "50 Hz"}, - {freq: 100, label: "100 Hz"}, - {freq: 200, label: "200 Hz"}, - {freq: 500, label: "500 Hz"}, - {freq: 1e3, label: "1 kHz"}, - {freq: 2e3, label: "2 kHz"}, - {freq: 5e3, label: "5 kHz"}, - {freq: 1e4, label: "10 kHz"}, - {freq: 2e4, label: "20 kHz"}, - } { - x := float32(math.Log10(p.freq / 22050)) - if x >= r.a && x <= r.b { - yield(x, p.label) + const offset = 0.343408593803857 // log10(22050/10000) + const startdiv = 3 * (1 << 8) + step := nextPowerOfTwo(int(float64(r.b-r.a)*startdiv/float64(count)) + 1) + start := int(math.Floor(float64(r.a+offset) * startdiv / float64(step))) + end := int(math.Ceil(float64(r.b+offset) * startdiv / float64(step))) + for i := start; i <= end; i++ { + lognormfreq := float32(i*step)/startdiv - offset + freq := math.Pow(10, float64(lognormfreq)) * 22050 + df := freq * math.Log(10) * float64(step) / startdiv // this is roughly the difference in Hz between the ticks currently + rounding := int(math.Floor(math.Log10(df))) + r := math.Pow(10, float64(rounding)) + freq = math.Round(freq/r) * r + tickpos := float32(math.Log10(freq / 22050)) + if rounding >= 3 { + yield(tickpos, fmt.Sprintf("%.0f kHz", freq/1000)) + } else { + yield(tickpos, fmt.Sprintf("%s Hz", strconv.FormatFloat(freq, 'f', -rounding, 64))) } } } @@ -145,30 +136,47 @@ func (s *SpectrumState) Layout(gtx C) D { 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 + start = int(math.Ceil(float64(r.b) / float64(step))) + end = int(math.Floor(float64(r.a) / float64(step))) if end-start+1 <= count*4 { // we use 4x density for the y-lines in the spectrum break } + step *= 2 } for i := start; i <= end; i++ { - yield(float32(i*step)+SpectrumDisplayDb, strconv.Itoa(i*step)) + yield(float32(i*step), strconv.Itoa(i*step)) } } n := numchns if biquadok { n = 3 } - return s.plot.Layout(gtx, data, xticks, yticks, 0, n) + return s.plot.Layout(gtx, data, xticks, yticks, float32(math.NaN()), n) }), layout.Rigid(func(gtx C) D { return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, layout.Rigid(leftSpacer), + layout.Rigid(Label(t.Theme, &t.Theme.SongPanel.RowHeader, "Resolution").Layout), + layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }), + layout.Rigid(resolution.Layout), + layout.Rigid(rightSpacer), + ) + }), + layout.Rigid(func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(leftSpacer), + layout.Rigid(Label(t.Theme, &t.Theme.SongPanel.RowHeader, "Speed").Layout), + layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }), + layout.Rigid(speed.Layout), + layout.Rigid(rightSpacer), + ) + }), + layout.Rigid(func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(leftSpacer), + layout.Rigid(Label(t.Theme, &t.Theme.SongPanel.RowHeader, "Channels").Layout), layout.Flexed(1, func(gtx C) D { return D{Size: gtx.Constraints.Min} }), layout.Rigid(chnModeBtn.Layout), - layout.Rigid(smoothBtn.Layout), - layout.Rigid(resolution.Layout), layout.Rigid(rightSpacer), ) }), @@ -184,13 +192,26 @@ func smoothInterpolate(a, b float32, t float32) float32 { return (1-t)*a + t*b } +func nextPowerOfTwo(v int) int { + if v <= 0 { + return 1 + } + v-- + v |= v >> 1 + v |= v >> 2 + v |= v >> 4 + v |= v >> 8 + v |= v >> 16 + v |= v >> 32 + v++ + return v +} + func (s *SpectrumState) Update(gtx C) { t := TrackerFromContext(gtx) for s.chnModeBtn.Clicked(gtx) { t.Model.SpecAnChannelsInt().SetValue((t.SpecAnChannelsInt().Value() + 1) % int(tracker.NumSpecChnModes)) } - for s.smoothingBtn.Clicked(gtx) { - r := t.Model.SpecAnSmoothing().Range() - t.Model.SpecAnSmoothing().SetValue((t.SpecAnSmoothing().Value()+1)%(r.Max-r.Min+1) + r.Min) - } + s.resolutionNumber.Update(gtx, t.Model.SpecAnResolution()) + s.speed.Update(gtx, t.Model.SpecAnSpeed()) } diff --git a/tracker/int.go b/tracker/int.go index 03bd109..a96eadd 100644 --- a/tracker/int.go +++ b/tracker/int.go @@ -34,7 +34,7 @@ type ( Octave Model DetectorWeighting Model SyntherIndex Model - SpecAnSmoothing Model + SpecAnSpeed Model SpecAnResolution Model SpecAnChannelsInt Model ) @@ -86,7 +86,7 @@ func (m *Model) Step() Int { return MakeInt((*Step)(m)) } func (m *Model) Octave() Int { return MakeInt((*Octave)(m)) } func (m *Model) DetectorWeighting() Int { return MakeInt((*DetectorWeighting)(m)) } func (m *Model) SyntherIndex() Int { return MakeInt((*SyntherIndex)(m)) } -func (m *Model) SpecAnSmoothing() Int { return MakeInt((*SpecAnSmoothing)(m)) } +func (m *Model) SpecAnSpeed() Int { return MakeInt((*SpecAnSpeed)(m)) } func (m *Model) SpecAnResolution() Int { return MakeInt((*SpecAnResolution)(m)) } func (m *Model) SpecAnChannelsInt() Int { return MakeInt((*SpecAnChannelsInt)(m)) } @@ -158,13 +158,13 @@ func (v *DetectorWeighting) Range() IntRange { return IntRange{0, int(NumLoudnes // SpecAn stuff -func (v *SpecAnSmoothing) Value() int { return int(v.specAnSettings.Smooth) } -func (v *SpecAnSmoothing) SetValue(value int) bool { - v.specAnSettings.Smooth = SpecSmoothing(value) +func (v *SpecAnSpeed) Value() int { return int(v.specAnSettings.Smooth) } +func (v *SpecAnSpeed) SetValue(value int) bool { + v.specAnSettings.Smooth = value TrySend(v.broker.ToSpecAn, MsgToSpecAn{HasSettings: true, SpecSettings: v.specAnSettings}) return true } -func (v *SpecAnSmoothing) Range() IntRange { return IntRange{0, int(NumSpecSmoothing) - 1} } +func (v *SpecAnSpeed) Range() IntRange { return IntRange{SpecSpeedMin, SpecSpeedMax} } func (v *SpecAnResolution) Value() int { return v.specAnSettings.Resolution } func (v *SpecAnResolution) SetValue(value int) bool { diff --git a/tracker/spectrum.go b/tracker/spectrum.go index 2017ba8..d7f67e1 100644 --- a/tracker/spectrum.go +++ b/tracker/spectrum.go @@ -18,13 +18,12 @@ type ( SpecAnSettings struct { ChnMode SpecChnMode - Smooth SpecSmoothing + Smooth int Resolution int } - SpecChnMode int - SpecSmoothing int - Spectrum [2][]float32 + SpecChnMode int + Spectrum [2][]float32 specTemp struct { power [2][]float32 @@ -42,38 +41,24 @@ type ( ) const ( - SpecResolutionMin = 7 - SpecResolutionMax = 16 + SpecResolutionMin = -3 + SpecResolutionMax = 3 ) const ( - SpecChnModeOff SpecChnMode = iota // no spectrum analysis is done to save CPU resources - SpecChnModeCombine // calculate a single combined spectrum for both channels + SpecSpeedMin = -3 + SpecSpeedMax = 3 +) + +const ( + SpecChnModeSum SpecChnMode = iota // calculate a single combined spectrum for both channels SpecChnModeSeparate // calculate separate spectrums for left and right channels NumSpecChnModes ) -const ( - SpecSmoothingMedium SpecSmoothing = iota - SpecSmoothingFast - SpecSmoothingSlow - - NumSpecSmoothing -) - -var spectrumSmoothingMap map[SpecSmoothing]float32 = map[SpecSmoothing]float32{ - SpecSmoothingSlow: 0.1, - SpecSmoothingMedium: 0.2, - SpecSmoothingFast: 0.4, -} - func NewSpecAnalyzer(broker *Broker) *SpecAnalyzer { ret := &SpecAnalyzer{broker: broker} - ret.init(SpecAnSettings{ - ChnMode: SpecChnModeCombine, - Smooth: SpecSmoothingMedium, - Resolution: 10, - }) + ret.init(SpecAnSettings{}) return ret } @@ -87,23 +72,28 @@ func (m *Model) BiquadCoeffs() (coeffs BiquadCoeffs, ok bool) { 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)) + f *= f + r := float32(p["resonance"]) / 128 + // in state-space, the filter has the form: + // s(n+1) = A*s(n)+B*u, where A = [1 f;-f 1-f*r-f*f] and B = [0;f] + // y(n) = C*s(n)+D*u, where + // C = [low band] + // + // The transfer function is then H(z) = C*(zI-A)^-1*B + D + // z*I-A = [z-1 -f; f z+f*r+f*f-1] + // Invert it: + // (z*I-A)^-1 = 1/det * [z+f*r+f*f-1 f; -f z-1], where det = (z-1)*(z+f*r+f*f-1)+f^2 = z^2 + z * (f*r+f*f−2) + 1-f*r + // (z*I-A)^-1*B = 1/det * [-f*f; f*z-f] + var a0 float32 = 1 + var a1 float32 = r*f + f*f - 2 + var a2 float32 = 1 - r*f var b0, b1, b2 float32 - if p["low"] == 1 { - b0 += g * g - b1 += 2 * g * g - b2 += g * g + if p["lowpass"] == 1 { + b2 = -f * f } - 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 + b2 -= f * float32(p["bandpass"]) + b1 += f * float32(p["bandpass"]) + return BiquadCoeffs{a0: a0, 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 @@ -149,14 +139,12 @@ func (s *SpecAnalyzer) handleMsg(msg MsgToSpecAn) { } switch m := msg.Data.(type) { case *sointu.AudioBuffer: - if s.settings.ChnMode != SpecChnModeOff { - buf := *m - l := len(s.temp.window) - // 50% overlap with the windows - s.chunker.Process(buf, l, l>>1, func(chunk sointu.AudioBuffer) { - TrySend(s.broker.ToModel, MsgToModel{Data: s.update(chunk)}) - }) - } + buf := *m + l := len(s.temp.window) + // 50% overlap with the windows + s.chunker.Process(buf, l, l>>1, func(chunk sointu.AudioBuffer) { + TrySend(s.broker.ToModel, MsgToModel{Data: s.update(chunk)}) + }) s.broker.PutAudioBuffer(m) default: // unknown message type; ignore @@ -164,7 +152,7 @@ func (s *SpecAnalyzer) handleMsg(msg MsgToSpecAn) { } func (a *SpecAnalyzer) init(s SpecAnSettings) { - s.Resolution = min(max(s.Resolution, SpecResolutionMin), SpecResolutionMax) + s.Resolution = min(max(s.Resolution, SpecResolutionMin), SpecResolutionMax) + 10 a.settings = s n := 1 << s.Resolution a.temp = specTemp{ @@ -205,7 +193,7 @@ func (s *SpecAnalyzer) update(buf sointu.AudioBuffer) *Spectrum { s.process(buf, 1) ret[0] = append(ret[0], s.temp.power[0]...) ret[1] = append(ret[1], s.temp.power[1]...) - case SpecChnModeCombine: + case SpecChnModeSum: s.process(buf, 0) s.process(buf, 1) ret[0] = append(ret[0], s.temp.power[0]...) @@ -260,7 +248,7 @@ func (sd *SpecAnalyzer) process(buf sointu.AudioBuffer, channel int) { vek32.MulNumber_Inplace(t2[:m-1], 2) // calculate difference to current spectrum and add back, multiplied by smoothing factor vek32.Sub_Inplace(t2, sd.temp.power[channel]) - alpha := spectrumSmoothingMap[sd.settings.Smooth] + alpha := float32(math.Pow(2, float64(sd.settings.Smooth-SpecSpeedMax))) vek32.MulNumber_Inplace(t2, alpha) vek32.Add_Inplace(sd.temp.power[channel], t2) }