mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
feat(tracker): buttons for loudness weighting and peak oversampling
Closes #186
This commit is contained in:
parent
805b98524c
commit
5fd78d8362
@ -5,8 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
- The loudness detection is now LUFS and peak detection is based on oversampled
|
- The loudness detection supports LUFS, A-weighting, C-weighting or
|
||||||
true peak detection
|
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])
|
- Oscilloscope to visualize the outputted waveform ([#61][i61])
|
||||||
- Toggle button to keep instruments and tracks linked, and buttons to to split
|
- Toggle button to keep instruments and tracks linked, and buttons to to split
|
||||||
instruments and tracks with more than 1 voice into parallel ones
|
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
|
[i166]: https://github.com/vsariola/sointu/issues/166
|
||||||
[i168]: https://github.com/vsariola/sointu/issues/168
|
[i168]: https://github.com/vsariola/sointu/issues/168
|
||||||
[i170]: https://github.com/vsariola/sointu/issues/170
|
[i170]: https://github.com/vsariola/sointu/issues/170
|
||||||
[i176]: https://github.com/vsariola/sointu/issues/176
|
[i176]: https://github.com/vsariola/sointu/issues/176
|
||||||
|
[i186]: https://github.com/vsariola/sointu/issues/186
|
||||||
|
@ -26,6 +26,7 @@ type (
|
|||||||
Mute Model
|
Mute Model
|
||||||
Solo Model
|
Solo Model
|
||||||
LinkInstrTrack Model
|
LinkInstrTrack Model
|
||||||
|
Oversampling Model
|
||||||
)
|
)
|
||||||
|
|
||||||
func (v Bool) Toggle() {
|
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) Mute() *Mute { return (*Mute)(m) }
|
||||||
func (m *Model) Solo() *Solo { return (*Solo)(m) }
|
func (m *Model) Solo() *Solo { return (*Solo)(m) }
|
||||||
func (m *Model) LinkInstrTrack() *LinkInstrTrack { return (*LinkInstrTrack)(m) }
|
func (m *Model) LinkInstrTrack() *LinkInstrTrack { return (*LinkInstrTrack)(m) }
|
||||||
|
func (m *Model) Oversampling() *Oversampling { return (*Oversampling)(m) }
|
||||||
|
|
||||||
// Panic methods
|
// Panic methods
|
||||||
|
|
||||||
@ -136,6 +138,16 @@ func (m *Effect) setValue(val bool) {
|
|||||||
}
|
}
|
||||||
func (m *Effect) Enabled() bool { return true }
|
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
|
// UnitSearching methods
|
||||||
|
|
||||||
func (m *UnitSearching) Bool() Bool { return Bool{m} }
|
func (m *UnitSearching) Bool() Bool { return Bool{m} }
|
||||||
|
@ -51,6 +51,11 @@ type (
|
|||||||
Reset bool
|
Reset bool
|
||||||
Quit 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/
|
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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -89,6 +89,7 @@ const (
|
|||||||
AWeighting
|
AWeighting
|
||||||
CWeighting
|
CWeighting
|
||||||
NoWeighting
|
NoWeighting
|
||||||
|
NumWeightingTypes
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDetector(b *Broker) *Detector {
|
func NewDetector(b *Broker) *Detector {
|
||||||
@ -109,6 +110,15 @@ func (s *Detector) Run() {
|
|||||||
if msg.Quit {
|
if msg.Quit {
|
||||||
return
|
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) {
|
switch data := msg.Data.(type) {
|
||||||
case *sointu.AudioBuffer:
|
case *sointu.AudioBuffer:
|
||||||
buf := *data
|
buf := *data
|
||||||
@ -367,7 +377,12 @@ func (d *peakDetector) update(buf sointu.AudioBuffer) (ret PeakResult) {
|
|||||||
d.tmp[i] = buf[i][chn]
|
d.tmp[i] = buf[i][chn]
|
||||||
}
|
}
|
||||||
// 4x oversample the signal
|
// 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
|
// take absolute value of the oversampled signal
|
||||||
vek32.Abs_Inplace(o)
|
vek32.Abs_Inplace(o)
|
||||||
p := vek32.Max(o)
|
p := vek32.Max(o)
|
||||||
|
@ -23,6 +23,9 @@ type SongPanel struct {
|
|||||||
LoudnessExpander *Expander
|
LoudnessExpander *Expander
|
||||||
PeakExpander *Expander
|
PeakExpander *Expander
|
||||||
|
|
||||||
|
WeightingTypeBtn *Clickable
|
||||||
|
OversamplingBtn *Clickable
|
||||||
|
|
||||||
BPM *NumberInput
|
BPM *NumberInput
|
||||||
RowsPerPattern *NumberInput
|
RowsPerPattern *NumberInput
|
||||||
RowsPerBeat *NumberInput
|
RowsPerBeat *NumberInput
|
||||||
@ -46,6 +49,9 @@ func NewSongPanel(model *tracker.Model) *SongPanel {
|
|||||||
MenuBar: NewMenuBar(model),
|
MenuBar: NewMenuBar(model),
|
||||||
PlayBar: NewPlayBar(model),
|
PlayBar: NewPlayBar(model),
|
||||||
|
|
||||||
|
WeightingTypeBtn: &Clickable{},
|
||||||
|
OversamplingBtn: &Clickable{},
|
||||||
|
|
||||||
SongSettingsExpander: &Expander{Expanded: true},
|
SongSettingsExpander: &Expander{Expanded: true},
|
||||||
ScopeExpander: &Expander{},
|
ScopeExpander: &Expander{},
|
||||||
LoudnessExpander: &Expander{},
|
LoudnessExpander: &Expander{},
|
||||||
@ -54,7 +60,17 @@ func NewSongPanel(model *tracker.Model) *SongPanel {
|
|||||||
return ret
|
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 {
|
func (s *SongPanel) Layout(gtx C, t *Tracker) D {
|
||||||
|
s.Update(gtx, t)
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return s.MenuBar.Layout(gtx, t)
|
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)
|
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,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return t.SongSettingsExpander.Layout(gtx, tr.Theme, "Song",
|
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)
|
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 {
|
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 {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Momentary", dbLabel(tr.Theme, tr.Model.DetectorResult().Loudness[tracker.LoudnessMomentary]).Layout)
|
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 {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Max. short term", dbLabel(tr.Theme, tr.Model.DetectorResult().Loudness[tracker.LoudnessMaxShortTerm]).Layout)
|
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)
|
return dbLabel(tr.Theme, maxPeak).Layout(gtx)
|
||||||
},
|
},
|
||||||
func(gtx C) D {
|
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
|
// no need to show momentary peak, it does not have too much meaning
|
||||||
layout.Rigid(func(gtx C) D {
|
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)
|
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 {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layoutSongOptionRow(gtx, tr.Theme, "Integrated R", dbLabel(tr.Theme, tr.Model.DetectorResult().Peaks[tracker.PeakIntegrated][1]).Layout)
|
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)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -29,6 +29,8 @@ type (
|
|||||||
RowsPerBeat Model
|
RowsPerBeat Model
|
||||||
Step Model
|
Step Model
|
||||||
Octave Model
|
Octave Model
|
||||||
|
|
||||||
|
DetectorWeighting Model
|
||||||
)
|
)
|
||||||
|
|
||||||
func (v Int) Add(delta int) (ok bool) {
|
func (v Int) Add(delta int) (ok bool) {
|
||||||
@ -59,14 +61,15 @@ func (r intRange) Clamp(value int) int {
|
|||||||
|
|
||||||
// Model methods
|
// Model methods
|
||||||
|
|
||||||
func (m *Model) InstrumentVoices() *InstrumentVoices { return (*InstrumentVoices)(m) }
|
func (m *Model) InstrumentVoices() *InstrumentVoices { return (*InstrumentVoices)(m) }
|
||||||
func (m *Model) TrackVoices() *TrackVoices { return (*TrackVoices)(m) }
|
func (m *Model) TrackVoices() *TrackVoices { return (*TrackVoices)(m) }
|
||||||
func (m *Model) SongLength() *SongLength { return (*SongLength)(m) }
|
func (m *Model) SongLength() *SongLength { return (*SongLength)(m) }
|
||||||
func (m *Model) BPM() *BPM { return (*BPM)(m) }
|
func (m *Model) BPM() *BPM { return (*BPM)(m) }
|
||||||
func (m *Model) RowsPerPattern() *RowsPerPattern { return (*RowsPerPattern)(m) }
|
func (m *Model) RowsPerPattern() *RowsPerPattern { return (*RowsPerPattern)(m) }
|
||||||
func (m *Model) RowsPerBeat() *RowsPerBeat { return (*RowsPerBeat)(m) }
|
func (m *Model) RowsPerBeat() *RowsPerBeat { return (*RowsPerBeat)(m) }
|
||||||
func (m *Model) Step() *Step { return (*Step)(m) }
|
func (m *Model) Step() *Step { return (*Step)(m) }
|
||||||
func (m *Model) Octave() *Octave { return (*Octave)(m) }
|
func (m *Model) Octave() *Octave { return (*Octave)(m) }
|
||||||
|
func (m *Model) DetectorWeighting() *DetectorWeighting { return (*DetectorWeighting)(m) }
|
||||||
|
|
||||||
// BeatsPerMinuteInt
|
// BeatsPerMinuteInt
|
||||||
|
|
||||||
@ -126,6 +129,17 @@ func (v *RowsPerBeat) change(kind string) func() {
|
|||||||
return (*Model)(v).change("RowsPerBeatInt."+kind, SongChange, MinorChange)
|
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
|
// InstrumentVoicesInt
|
||||||
|
|
||||||
func (v *InstrumentVoices) Int() Int {
|
func (v *InstrumentVoices) Int() Int {
|
||||||
|
@ -73,6 +73,9 @@ type (
|
|||||||
signalAnalyzer *ScopeModel
|
signalAnalyzer *ScopeModel
|
||||||
detectorResult DetectorResult
|
detectorResult DetectorResult
|
||||||
|
|
||||||
|
weightingType WeightingType
|
||||||
|
oversampling bool
|
||||||
|
|
||||||
alerts []Alert
|
alerts []Alert
|
||||||
dialog Dialog
|
dialog Dialog
|
||||||
synther sointu.Synther // the synther used to create new synths
|
synther sointu.Synther // the synther used to create new synths
|
||||||
|
Loading…
Reference in New Issue
Block a user