From 5fd78d8362a71ad6012ac98967d2d6bab75e474a Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Sun, 27 Apr 2025 21:30:10 +0300 Subject: [PATCH] feat(tracker): buttons for loudness weighting and peak oversampling Closes #186 --- CHANGELOG.md | 8 +++--- tracker/bool.go | 12 +++++++++ tracker/broker.go | 5 ++++ tracker/detector.go | 17 ++++++++++++- tracker/gioui/songpanel.go | 50 ++++++++++++++++++++++++++++++++++++-- tracker/int.go | 30 +++++++++++++++++------ tracker/model.go | 3 +++ 7 files changed, 111 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc05689..d28a098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added -- The loudness detection is now LUFS and peak detection is based on oversampled - true peak detection +- The loudness detection supports LUFS, A-weighting, C-weighting or + RMS-weighting, and peak detection supports true peak or sample peak detection. + The loudness and peak values are displayed in the song panel ([#186][i186]) - Oscilloscope to visualize the outputted waveform ([#61][i61]) - Toggle button to keep instruments and tracks linked, and buttons to to split instruments and tracks with more than 1 voice into parallel ones @@ -320,4 +321,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). [i166]: https://github.com/vsariola/sointu/issues/166 [i168]: https://github.com/vsariola/sointu/issues/168 [i170]: https://github.com/vsariola/sointu/issues/170 -[i176]: https://github.com/vsariola/sointu/issues/176 \ No newline at end of file +[i176]: https://github.com/vsariola/sointu/issues/176 +[i186]: https://github.com/vsariola/sointu/issues/186 diff --git a/tracker/bool.go b/tracker/bool.go index 3c32b5a..af3acfe 100644 --- a/tracker/bool.go +++ b/tracker/bool.go @@ -26,6 +26,7 @@ type ( Mute Model Solo Model LinkInstrTrack Model + Oversampling Model ) func (v Bool) Toggle() { @@ -55,6 +56,7 @@ func (m *Model) UniquePatterns() *UniquePatterns { return (*UniquePatterns)(m) func (m *Model) Mute() *Mute { return (*Mute)(m) } func (m *Model) Solo() *Solo { return (*Solo)(m) } func (m *Model) LinkInstrTrack() *LinkInstrTrack { return (*LinkInstrTrack)(m) } +func (m *Model) Oversampling() *Oversampling { return (*Oversampling)(m) } // Panic methods @@ -136,6 +138,16 @@ func (m *Effect) setValue(val bool) { } func (m *Effect) Enabled() bool { return true } +// Oversampling methods + +func (m *Oversampling) Bool() Bool { return Bool{m} } +func (m *Oversampling) Value() bool { return m.oversampling } +func (m *Oversampling) setValue(val bool) { + m.oversampling = val + trySend(m.broker.ToDetector, MsgToDetector{HasOversampling: true, Oversampling: val}) +} +func (m *Oversampling) Enabled() bool { return true } + // UnitSearching methods func (m *UnitSearching) Bool() Bool { return Bool{m} } diff --git a/tracker/broker.go b/tracker/broker.go index dacae6b..25a2ef8 100644 --- a/tracker/broker.go +++ b/tracker/broker.go @@ -51,6 +51,11 @@ type ( Reset bool Quit bool Data any // TODO: consider using a sum type here, for a bit more type safety. See: https://www.jerf.org/iri/post/2917/ + + WeightingType WeightingType + HasWeightingType bool + Oversampling bool + HasOversampling bool } ) diff --git a/tracker/detector.go b/tracker/detector.go index d425b04..0a82ba2 100644 --- a/tracker/detector.go +++ b/tracker/detector.go @@ -89,6 +89,7 @@ const ( AWeighting CWeighting NoWeighting + NumWeightingTypes ) func NewDetector(b *Broker) *Detector { @@ -109,6 +110,15 @@ func (s *Detector) Run() { if msg.Quit { return } + if msg.HasWeightingType { + s.loudnessDetector.weighting = weightings[WeightingType(msg.WeightingType)] + s.loudnessDetector.reset() + } + if msg.HasOversampling { + s.peakDetector.oversampling = msg.Oversampling + s.peakDetector.reset() + } + switch data := msg.Data.(type) { case *sointu.AudioBuffer: buf := *data @@ -367,7 +377,12 @@ func (d *peakDetector) update(buf sointu.AudioBuffer) (ret PeakResult) { d.tmp[i] = buf[i][chn] } // 4x oversample the signal - o := d.states[chn].Oversample(d.tmp[:len(buf)], d.tmp2) + var o []float32 + if d.oversampling { + o = d.states[chn].Oversample(d.tmp[:len(buf)], d.tmp2) + } else { + o = d.tmp[:len(buf)] + } // take absolute value of the oversampled signal vek32.Abs_Inplace(o) p := vek32.Max(o) diff --git a/tracker/gioui/songpanel.go b/tracker/gioui/songpanel.go index 2e359e0..8c9e1ed 100644 --- a/tracker/gioui/songpanel.go +++ b/tracker/gioui/songpanel.go @@ -23,6 +23,9 @@ type SongPanel struct { LoudnessExpander *Expander PeakExpander *Expander + WeightingTypeBtn *Clickable + OversamplingBtn *Clickable + BPM *NumberInput RowsPerPattern *NumberInput RowsPerBeat *NumberInput @@ -46,6 +49,9 @@ func NewSongPanel(model *tracker.Model) *SongPanel { MenuBar: NewMenuBar(model), PlayBar: NewPlayBar(model), + WeightingTypeBtn: &Clickable{}, + OversamplingBtn: &Clickable{}, + SongSettingsExpander: &Expander{Expanded: true}, ScopeExpander: &Expander{}, LoudnessExpander: &Expander{}, @@ -54,7 +60,17 @@ func NewSongPanel(model *tracker.Model) *SongPanel { return ret } +func (s *SongPanel) Update(gtx C, t *Tracker) { + for s.WeightingTypeBtn.Clicked(gtx) { + t.Model.DetectorWeighting().Int().Set((t.DetectorWeighting().Value() + 1) % int(tracker.NumWeightingTypes)) + } + for s.OversamplingBtn.Clicked(gtx) { + t.Model.Oversampling().Bool().Set(!t.Oversampling().Value()) + } +} + func (s *SongPanel) Layout(gtx C, t *Tracker) D { + s.Update(gtx, t) return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { return s.MenuBar.Layout(gtx, t) @@ -83,6 +99,28 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D { scopeStyle := LineOscilloscope(t.Scope, tr.SignalAnalyzer().Waveform(), tr.Theme) + var weightingTxt string + switch tracker.WeightingType(tr.Model.DetectorWeighting().Value()) { + case tracker.KWeighting: + weightingTxt = "K-weight (LUFS)" + case tracker.AWeighting: + weightingTxt = "A-weight" + case tracker.CWeighting: + weightingTxt = "C-weight" + case tracker.NoWeighting: + weightingTxt = "No weight (RMS)" + } + + weightingBtn := LowEmphasisButton(tr.Theme, t.WeightingTypeBtn, weightingTxt) + weightingBtn.Color = mediumEmphasisTextColor + + oversamplingTxt := "Sample peak" + if tr.Model.Oversampling().Value() { + oversamplingTxt = "True peak" + } + oversamplingBtn := LowEmphasisButton(tr.Theme, t.OversamplingBtn, oversamplingTxt) + oversamplingBtn.Color = mediumEmphasisTextColor + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, layout.Rigid(func(gtx C) D { return t.SongSettingsExpander.Layout(gtx, tr.Theme, "Song", @@ -115,7 +153,7 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D { return LabelStyle{Text: fmt.Sprintf("%.1f dB", tr.Model.DetectorResult().Loudness[tracker.LoudnessShortTerm]), Color: mediumEmphasisTextColor, Alignment: layout.W, FontSize: tr.Theme.TextSize * 14.0 / 16.0, Shaper: tr.Theme.Shaper}.Layout(gtx) }, func(gtx C) D { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx, layout.Rigid(func(gtx C) D { return layoutSongOptionRow(gtx, tr.Theme, "Momentary", dbLabel(tr.Theme, tr.Model.DetectorResult().Loudness[tracker.LoudnessMomentary]).Layout) }), @@ -131,6 +169,10 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D { layout.Rigid(func(gtx C) D { return layoutSongOptionRow(gtx, tr.Theme, "Max. short term", dbLabel(tr.Theme, tr.Model.DetectorResult().Loudness[tracker.LoudnessMaxShortTerm]).Layout) }), + layout.Rigid(func(gtx C) D { + gtx.Constraints.Min.X = 0 + return weightingBtn.Layout(gtx) + }), ) }, ) @@ -142,7 +184,7 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D { return dbLabel(tr.Theme, maxPeak).Layout(gtx) }, func(gtx C) D { - return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx, // no need to show momentary peak, it does not have too much meaning layout.Rigid(func(gtx C) D { return layoutSongOptionRow(gtx, tr.Theme, "Short term L", dbLabel(tr.Theme, tr.Model.DetectorResult().Peaks[tracker.PeakShortTerm][0]).Layout) @@ -156,6 +198,10 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D { layout.Rigid(func(gtx C) D { return layoutSongOptionRow(gtx, tr.Theme, "Integrated R", dbLabel(tr.Theme, tr.Model.DetectorResult().Peaks[tracker.PeakIntegrated][1]).Layout) }), + layout.Rigid(func(gtx C) D { + gtx.Constraints.Min.X = 0 + return oversamplingBtn.Layout(gtx) + }), ) }, ) diff --git a/tracker/int.go b/tracker/int.go index bb08564..da95040 100644 --- a/tracker/int.go +++ b/tracker/int.go @@ -29,6 +29,8 @@ type ( RowsPerBeat Model Step Model Octave Model + + DetectorWeighting Model ) func (v Int) Add(delta int) (ok bool) { @@ -59,14 +61,15 @@ func (r intRange) Clamp(value int) int { // Model methods -func (m *Model) InstrumentVoices() *InstrumentVoices { return (*InstrumentVoices)(m) } -func (m *Model) TrackVoices() *TrackVoices { return (*TrackVoices)(m) } -func (m *Model) SongLength() *SongLength { return (*SongLength)(m) } -func (m *Model) BPM() *BPM { return (*BPM)(m) } -func (m *Model) RowsPerPattern() *RowsPerPattern { return (*RowsPerPattern)(m) } -func (m *Model) RowsPerBeat() *RowsPerBeat { return (*RowsPerBeat)(m) } -func (m *Model) Step() *Step { return (*Step)(m) } -func (m *Model) Octave() *Octave { return (*Octave)(m) } +func (m *Model) InstrumentVoices() *InstrumentVoices { return (*InstrumentVoices)(m) } +func (m *Model) TrackVoices() *TrackVoices { return (*TrackVoices)(m) } +func (m *Model) SongLength() *SongLength { return (*SongLength)(m) } +func (m *Model) BPM() *BPM { return (*BPM)(m) } +func (m *Model) RowsPerPattern() *RowsPerPattern { return (*RowsPerPattern)(m) } +func (m *Model) RowsPerBeat() *RowsPerBeat { return (*RowsPerBeat)(m) } +func (m *Model) Step() *Step { return (*Step)(m) } +func (m *Model) Octave() *Octave { return (*Octave)(m) } +func (m *Model) DetectorWeighting() *DetectorWeighting { return (*DetectorWeighting)(m) } // BeatsPerMinuteInt @@ -126,6 +129,17 @@ func (v *RowsPerBeat) change(kind string) func() { return (*Model)(v).change("RowsPerBeatInt."+kind, SongChange, MinorChange) } +// ModelLoudnessType + +func (v *DetectorWeighting) Int() Int { return Int{v} } +func (v *DetectorWeighting) Value() int { return int(v.weightingType) } +func (v *DetectorWeighting) setValue(value int) { + v.weightingType = WeightingType(value) + trySend(v.broker.ToDetector, MsgToDetector{HasWeightingType: true, WeightingType: WeightingType(value)}) +} +func (v *DetectorWeighting) Range() intRange { return intRange{0, int(NumLoudnessTypes) - 1} } +func (v *DetectorWeighting) change(kind string) func() { return func() {} } + // InstrumentVoicesInt func (v *InstrumentVoices) Int() Int { diff --git a/tracker/model.go b/tracker/model.go index 89dbf19..0f557a1 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -73,6 +73,9 @@ type ( signalAnalyzer *ScopeModel detectorResult DetectorResult + weightingType WeightingType + oversampling bool + alerts []Alert dialog Dialog synther sointu.Synther // the synther used to create new synths