diff --git a/patch.go b/patch.go index 0d1f72c..370cdda 100644 --- a/patch.go +++ b/patch.go @@ -251,23 +251,12 @@ func arrDispFunc(arr []string) UnitParameterDisplayFunc { } func filterFrequencyDispFunc(v int) (string, string) { - // Matlab was used to find the frequency for the singularity when r = 0: - // % p is the frequency parameter squared, p = freq * freq - // % We assume the singular case r = 0. - // syms p z s T - // A = [1 p;-p 1-p*p]; % discrete state-space matrix x(k+1)=A*x(k) + ... - // pol = det(z*eye(2)-A) % characteristic discrete polynomial - // spol = simplify(subs(pol,z,(1+s*T/2)/(1-s*T/2))) % Tustin approximation - // % where T = 1/(44100 Hz) is the sample period - // % spol is of the form N(s)/D(s), where N(s)=(-T^2*p^2*s^2+4*T^2*s^2+4*p^2) - // % We are interested in the roots i.e. when spol == 0 <=> N(s)==0 - // simplify(solve((-T^2*p^2*s^2+4*T^2*s^2+4*p^2)==0,s)) - // % Answer: s=±2*p/(T*(p^2-4)^(1/2)). For small p, this simplifies to: - // % s=±p*j/T. Thus, s=j*omega=j*2*pi*f => f=±p/(2*pi*T). - // So the singularity is when f = p / (2*pi*T) Hz. + // In https://www.musicdsp.org/en/latest/Filters/23-state-variable.html, + // they call it "cutoff" but it's actually the location of the resonance + // peak freq := float64(v) / 128 p := freq * freq - f := 44100 * p / math.Pi / 2 + f := math.Asin(p/2) / math.Pi * 44100 return strconv.FormatFloat(f, 'f', 0, 64), "Hz" } diff --git a/tracker/gioui/song_panel.go b/tracker/gioui/song_panel.go index ecf0b76..82f91d6 100644 --- a/tracker/gioui/song_panel.go +++ b/tracker/gioui/song_panel.go @@ -38,6 +38,9 @@ type SongPanel struct { Step *NumericUpDownState SongLength *NumericUpDownState + List *layout.List + ScrollBar *ScrollBar + Scope *OscilloscopeState SpectrumState *SpectrumState @@ -68,6 +71,9 @@ func NewSongPanel(tr *Tracker) *SongPanel { CPUExpander: &Expander{}, SpectrumExpander: &Expander{}, + List: &layout.List{Axis: layout.Vertical}, + ScrollBar: &ScrollBar{Axis: layout.Vertical}, + SpectrumState: NewSpectrumState(), } return ret @@ -158,8 +164,9 @@ func (t *SongPanel) layoutSongOptions(gtx C) D { synthBtn := Btn(tr.Theme, &tr.Theme.Button.Text, t.SynthBtn, tr.Model.SyntherName(), "") - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - layout.Rigid(func(gtx C) D { + listItem := func(gtx C, index int) D { + switch index { + case 0: return t.SongSettingsExpander.Layout(gtx, tr.Theme, "Song", func(gtx C) D { return Label(tr.Theme, &tr.Theme.SongPanel.RowHeader, strconv.Itoa(tr.BPM().Value())+" BPM").Layout(gtx) @@ -188,8 +195,7 @@ func (t *SongPanel) layoutSongOptions(gtx C) D { }), ) }) - }), - layout.Rigid(func(gtx C) D { + case 1: return t.CPUExpander.Layout(gtx, tr.Theme, "CPU", cpuSmallLabel, func(gtx C) D { return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx, @@ -198,8 +204,7 @@ func (t *SongPanel) layoutSongOptions(gtx C) D { ) }, ) - }), - layout.Rigid(func(gtx C) D { + case 2: return t.LoudnessExpander.Layout(gtx, tr.Theme, "Loudness", func(gtx C) D { loudness := tr.Model.DetectorResult().Loudness[tracker.LoudnessShortTerm] @@ -229,8 +234,7 @@ func (t *SongPanel) layoutSongOptions(gtx C) D { ) }, ) - }), - layout.Rigid(func(gtx C) D { + case 3: return t.PeakExpander.Layout(gtx, tr.Theme, "Peaks", func(gtx C) D { maxPeak := max(tr.Model.DetectorResult().Peaks[tracker.PeakShortTerm][0], tr.Model.DetectorResult().Peaks[tracker.PeakShortTerm][1]) @@ -258,16 +262,24 @@ func (t *SongPanel) layoutSongOptions(gtx C) D { ) }, ) - }), - layout.Flexed(1, func(gtx C) D { + case 4: + gtx.Constraints.Max.Y = gtx.Dp(300) scope := Scope(tr.Theme, tr.Model.SignalAnalyzer(), t.Scope) return t.ScopeExpander.Layout(gtx, tr.Theme, "Oscilloscope", func(gtx C) D { return D{} }, scope.Layout) - }), - layout.Flexed(1, func(gtx C) D { + case 5: + gtx.Constraints.Max.Y = gtx.Dp(300) return t.SpectrumExpander.Layout(gtx, tr.Theme, "Spectrum", func(gtx C) D { return D{} }, t.SpectrumState.Layout) - }), - layout.Rigid(Label(tr.Theme, &tr.Theme.SongPanel.Version, version.VersionOrHash).Layout), - ) + case 6: + return Label(tr.Theme, &tr.Theme.SongPanel.Version, version.VersionOrHash).Layout(gtx) + default: + return D{} + } + } + gtx.Constraints.Min = gtx.Constraints.Max + dims := t.List.Layout(gtx, 7, listItem) + t.ScrollBar.Layout(gtx, &tr.Theme.ScrollBar, 7, &t.List.Position) + tr.SpecAnEnabled().SetValue(t.SpectrumExpander.Expanded) + return dims } func dbLabel(th *Theme, value tracker.Decibel) LabelWidget { diff --git a/tracker/gioui/specanalyzer.go b/tracker/gioui/specanalyzer.go index 917ed2a..56ffe55 100644 --- a/tracker/gioui/specanalyzer.go +++ b/tracker/gioui/specanalyzer.go @@ -48,7 +48,7 @@ func (s *SpectrumState) Layout(gtx C) D { } resolution := NumUpDown(t.Model.SpecAnResolution(), t.Theme, s.resolutionNumber, "Resolution") - chnModeBtn := Btn(t.Theme, &t.Theme.Button.Filled, s.chnModeBtn, chnModeTxt, "Channel mode") + chnModeBtn := Btn(t.Theme, &t.Theme.Button.Text, s.chnModeBtn, chnModeTxt, "Channel mode") speed := NumUpDown(t.Model.SpecAnSpeed(), t.Theme, s.speed, "Speed") numchns := 0 diff --git a/tracker/model.go b/tracker/model.go index 4398cf8..b26438a 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -79,6 +79,7 @@ type ( oversampling bool specAnSettings SpecAnSettings + specAnEnabled bool alerts []Alert dialog Dialog @@ -402,14 +403,16 @@ func (m *Model) ProcessMsg(msg MsgToModel) { case *sointu.AudioBuffer: m.signalAnalyzer.ProcessAudioBuffer(e) // chain the messages: when we have a new audio buffer, send them to the detector and the spectrum analyzer - clone := m.broker.GetAudioBuffer() - *clone = append(*clone, *e...) + if m.specAnEnabled { // send buffers to spectrum analyzer only if it's enabled + clone := m.broker.GetAudioBuffer() + *clone = append(*clone, *e...) + if !TrySend(m.broker.ToSpecAn, MsgToSpecAn{Data: clone}) { + m.broker.PutAudioBuffer(clone) + } + } if !TrySend(m.broker.ToDetector, MsgToDetector{Data: e}) { m.broker.PutAudioBuffer(e) } - if !TrySend(m.broker.ToSpecAn, MsgToSpecAn{Data: clone}) { - m.broker.PutAudioBuffer(clone) - } case *Spectrum: m.broker.PutSpectrum(m.spectrum) m.spectrum = e diff --git a/tracker/spectrum.go b/tracker/spectrum.go index 5114f7c..2adf09a 100644 --- a/tracker/spectrum.go +++ b/tracker/spectrum.go @@ -38,6 +38,8 @@ type ( b0, b1, b2 float32 a0, a1, a2 float32 } + + SpecAnEnabled Model ) const ( @@ -56,6 +58,8 @@ const ( NumSpecChnModes ) +func (m *Model) SpecAnEnabled() Bool { return MakeEnabledBool((*simpleBool)(&m.specAnEnabled)) } + func NewSpecAnalyzer(broker *Broker) *SpecAnalyzer { ret := &SpecAnalyzer{broker: broker} ret.init(SpecAnSettings{}) @@ -74,25 +78,29 @@ func (m *Model) BiquadCoeffs() (coeffs BiquadCoeffs, ok bool) { f := float32(p["frequency"]) / 128 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 = [1 0]*z, C_band = [0 1]*z, C_high = [-1 -f-r], D_high = [1] + // The equations for the filter are: + // s1[n+1] = s1[n] + f*s2[n] + // h = u - s1[n+1] - r*s2[n] + // s2[n+1] = s2[n] + f*h = s2[n] + f*(u-s1[n]-f*s2[n]-r*s2[n]) = - f*s1[n]+(1-f*r-f*f)*s2[n] + f*u + // y_low[n] = s1[n+1], y_band[n] = s2[n+1], y_high[n] = -s1[n+1]-r*s2[n]+u + // This gives state space representation + // 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 = [z 0], C_band = [0 z], C_high = [-z -r], D_high = [1] (note we use those z:s in C to account for those 1 sample time shifts) // 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*z+z*f*r+z*f*f-z-z-f*r-f*f+1+f^2 = z*z + z*(f*r+f*f-2)-f*r+1 - // (z*I-A)^-1*B = 1/det * f * [f; z-1] - // Low: z * [1,0] * f * [f;z-1] / det = f*f*z / det - // Band: z * [0,1] * f * [f;z-1] / det = (f*z^2-f*z) / det - // High: [-1,-f-r] * f * [f;z-1] / det + 1 = ((-f*f-r*f)*z+r*f)/det + 1 = ((-f*f-r*f)*z+r*f+det)/det = (z^2-2*z+1)/det + // z*I-A = [z-1 -f; f z+f*r+f*f-1] + // Calculate (zI-A)^-1*B: + // (z*I-A)^-1*B = 1/det * [z+f*r+f*f-1 f; -f z-1] * [0;f] = 1/det * f * [f; z-1], where + // det = (z+f*r+f*f-1)*(z-1)+f^2 = z*z+z*f*r+z*f*f-z-z-f*r-f*f+1+f^2 = z*z + (r*f+f*f-2)*z + 1-f*r = a0*z^2 + a1*z + a2 + // Low: [z 0]*f*[f;z-1] / det = f*f*z / det = b1 * z / det + // Band: [0 z]*f*[f;z-1] / det = (f*z^2-f*z) / det = (b0*z^2 + b1*z) / det + // High: [-z -r]*f*[f;z-1] / det + 1 = ((-f*f-r*f)*z+r*f)/det + 1 = ((-f*f-r*f)*z+r*f+det)/det = (z^2-2*z+1)/det = (b0*z^2 + b1*z + b2)/det + // Negative versions have only b coefficients negated var a0 float32 = 1 var a1 float32 = r*f + f*f - 2 var a2 float32 = 1 - f*r var b0, b1, b2 float32 - if p["lowpass"] == 1 { - b1 = f * f - } + b1 += f * f * float32(p["lowpass"]) b0 += f * float32(p["bandpass"]) b1 -= f * float32(p["bandpass"]) b0 += float32(p["highpass"])