mirror of
https://github.com/vsariola/sointu.git
synced 2026-02-12 19:23:12 -05:00
drafting
This commit is contained in:
parent
f765d75fde
commit
2303e89bbd
@ -52,7 +52,9 @@ func main() {
|
|||||||
model := tracker.NewModel(broker, cmd.Synthers, midiContext, recoveryFile)
|
model := tracker.NewModel(broker, cmd.Synthers, midiContext, recoveryFile)
|
||||||
player := tracker.NewPlayer(broker, cmd.Synthers[0])
|
player := tracker.NewPlayer(broker, cmd.Synthers[0])
|
||||||
detector := tracker.NewDetector(broker)
|
detector := tracker.NewDetector(broker)
|
||||||
|
specan := tracker.NewSpecAnalyzer(broker)
|
||||||
go detector.Run()
|
go detector.Run()
|
||||||
|
go specan.Run()
|
||||||
|
|
||||||
if a := flag.Args(); len(a) > 0 {
|
if a := flag.Args(); len(a) > 0 {
|
||||||
f, err := os.Open(a[0])
|
f, err := os.Open(a[0])
|
||||||
@ -72,7 +74,9 @@ func main() {
|
|||||||
trackerUi.Main()
|
trackerUi.Main()
|
||||||
audioCloser.Close()
|
audioCloser.Close()
|
||||||
tracker.TrySend(broker.CloseDetector, struct{}{})
|
tracker.TrySend(broker.CloseDetector, struct{}{})
|
||||||
|
tracker.TrySend(broker.CloseSpecAn, struct{}{})
|
||||||
tracker.TimeoutReceive(broker.FinishedDetector, 3*time.Second)
|
tracker.TimeoutReceive(broker.FinishedDetector, 3*time.Second)
|
||||||
|
tracker.TimeoutReceive(broker.FinishedSpecAn, 3*time.Second)
|
||||||
if *cpuprofile != "" {
|
if *cpuprofile != "" {
|
||||||
pprof.StopCPUProfile()
|
pprof.StopCPUProfile()
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|||||||
@ -49,7 +49,9 @@ func init() {
|
|||||||
model := tracker.NewModel(broker, cmd.Synthers, cmd.NewMidiContext(broker), recoveryFile)
|
model := tracker.NewModel(broker, cmd.Synthers, cmd.NewMidiContext(broker), recoveryFile)
|
||||||
player := tracker.NewPlayer(broker, cmd.Synthers[0])
|
player := tracker.NewPlayer(broker, cmd.Synthers[0])
|
||||||
detector := tracker.NewDetector(broker)
|
detector := tracker.NewDetector(broker)
|
||||||
|
specan := tracker.NewSpecAnalyzer(broker)
|
||||||
go detector.Run()
|
go detector.Run()
|
||||||
|
go specan.Run()
|
||||||
|
|
||||||
t := gioui.NewTracker(model)
|
t := gioui.NewTracker(model)
|
||||||
model.InstrEnlarged().SetValue(true)
|
model.InstrEnlarged().SetValue(true)
|
||||||
@ -112,8 +114,10 @@ func init() {
|
|||||||
CloseFunc: func() {
|
CloseFunc: func() {
|
||||||
tracker.TrySend(broker.CloseDetector, struct{}{})
|
tracker.TrySend(broker.CloseDetector, struct{}{})
|
||||||
tracker.TrySend(broker.CloseGUI, struct{}{})
|
tracker.TrySend(broker.CloseGUI, struct{}{})
|
||||||
|
tracker.TrySend(broker.CloseSpecAn, struct{}{})
|
||||||
tracker.TimeoutReceive(broker.FinishedDetector, 3*time.Second)
|
tracker.TimeoutReceive(broker.FinishedDetector, 3*time.Second)
|
||||||
tracker.TimeoutReceive(broker.FinishedGUI, 3*time.Second)
|
tracker.TimeoutReceive(broker.FinishedGUI, 3*time.Second)
|
||||||
|
tracker.TimeoutReceive(broker.FinishedSpecAn, 3*time.Second)
|
||||||
},
|
},
|
||||||
GetChunkFunc: func(isPreset bool) []byte {
|
GetChunkFunc: func(isPreset bool) []byte {
|
||||||
retChn := make(chan []byte)
|
retChn := make(chan []byte)
|
||||||
|
|||||||
@ -37,7 +37,7 @@ type (
|
|||||||
ToPlayer chan any // TODO: consider using a sum type here, for a bit more type safety. See: https://www.jerf.org/iri/post/2917/
|
ToPlayer chan any // TODO: consider using a sum type here, for a bit more type safety. See: https://www.jerf.org/iri/post/2917/
|
||||||
ToDetector chan MsgToDetector
|
ToDetector chan MsgToDetector
|
||||||
ToGUI chan any
|
ToGUI chan any
|
||||||
ToSpecAn chan any
|
ToSpecAn chan MsgToSpecAn
|
||||||
|
|
||||||
CloseDetector chan struct{}
|
CloseDetector chan struct{}
|
||||||
CloseGUI chan struct{}
|
CloseGUI chan struct{}
|
||||||
@ -53,7 +53,7 @@ type (
|
|||||||
mIDIEventsToGUI atomic.Bool
|
mIDIEventsToGUI atomic.Bool
|
||||||
|
|
||||||
bufferPool sync.Pool
|
bufferPool sync.Pool
|
||||||
f32slicePool sync.Pool
|
spectrumPool sync.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
// MsgToModel is a message sent to the model. The most often sent data
|
// MsgToModel is a message sent to the model. The most often sent data
|
||||||
@ -97,6 +97,12 @@ type (
|
|||||||
Param int
|
Param int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MsgToSpecAn struct {
|
||||||
|
SpecSettings SpecAnSettings
|
||||||
|
HasSettings bool
|
||||||
|
Data any
|
||||||
|
}
|
||||||
|
|
||||||
GUIMessageKind int
|
GUIMessageKind int
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -112,7 +118,7 @@ func NewBroker() *Broker {
|
|||||||
ToModel: make(chan MsgToModel, 1024),
|
ToModel: make(chan MsgToModel, 1024),
|
||||||
ToDetector: make(chan MsgToDetector, 1024),
|
ToDetector: make(chan MsgToDetector, 1024),
|
||||||
ToGUI: make(chan any, 1024),
|
ToGUI: make(chan any, 1024),
|
||||||
ToSpecAn: make(chan any, 1024),
|
ToSpecAn: make(chan MsgToSpecAn, 1024),
|
||||||
CloseDetector: make(chan struct{}, 1),
|
CloseDetector: make(chan struct{}, 1),
|
||||||
CloseGUI: make(chan struct{}, 1),
|
CloseGUI: make(chan struct{}, 1),
|
||||||
CloseSpecAn: make(chan struct{}, 1),
|
CloseSpecAn: make(chan struct{}, 1),
|
||||||
@ -120,7 +126,7 @@ func NewBroker() *Broker {
|
|||||||
FinishedDetector: make(chan struct{}),
|
FinishedDetector: make(chan struct{}),
|
||||||
FinishedSpecAn: make(chan struct{}),
|
FinishedSpecAn: make(chan struct{}),
|
||||||
bufferPool: sync.Pool{New: func() any { return &sointu.AudioBuffer{} }},
|
bufferPool: sync.Pool{New: func() any { return &sointu.AudioBuffer{} }},
|
||||||
f32slicePool: sync.Pool{New: func() any { return &[]float32{} }},
|
spectrumPool: sync.Pool{New: func() any { return &Spectrum{} }},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,15 +154,18 @@ func (b *Broker) PutAudioBuffer(buf *sointu.AudioBuffer) {
|
|||||||
b.bufferPool.Put(buf)
|
b.bufferPool.Put(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Broker) GetF32Slice(size int) *[]float32 {
|
func (b *Broker) GetSpectrum() *Spectrum {
|
||||||
return b.f32slicePool.Get().(*[]float32)
|
return b.spectrumPool.Get().(*Spectrum)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Broker) PutF32Slice(s *[]float32) {
|
func (b *Broker) PutSpectrum(s *Spectrum) {
|
||||||
if len(*s) > 0 {
|
if len((*s)[0]) > 0 {
|
||||||
*s = (*s)[:0]
|
(*s)[0] = (*s)[0][:0]
|
||||||
}
|
}
|
||||||
b.f32slicePool.Put(s)
|
if len((*s)[1]) > 0 {
|
||||||
|
(*s)[1] = (*s)[1][:0]
|
||||||
|
}
|
||||||
|
b.spectrumPool.Put(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrySend is a helper function to send a value to a channel if it is not full.
|
// TrySend is a helper function to send a value to a channel if it is not full.
|
||||||
|
|||||||
@ -12,7 +12,7 @@ type (
|
|||||||
broker *Broker
|
broker *Broker
|
||||||
loudnessDetector loudnessDetector
|
loudnessDetector loudnessDetector
|
||||||
peakDetector peakDetector
|
peakDetector peakDetector
|
||||||
chunkHistory sointu.AudioBuffer
|
chunker chunker
|
||||||
}
|
}
|
||||||
|
|
||||||
WeightingType int
|
WeightingType int
|
||||||
@ -136,26 +136,7 @@ func (s *Detector) handleMsg(msg MsgToDetector) {
|
|||||||
switch data := msg.Data.(type) {
|
switch data := msg.Data.(type) {
|
||||||
case *sointu.AudioBuffer:
|
case *sointu.AudioBuffer:
|
||||||
buf := *data
|
buf := *data
|
||||||
for len(buf) > 0 {
|
s.chunker.Process(buf, 4410, 0, func(chunk sointu.AudioBuffer) {
|
||||||
var chunk sointu.AudioBuffer
|
|
||||||
if len(s.chunkHistory) > 0 && len(s.chunkHistory) < 4410 {
|
|
||||||
l := min(len(buf), 4410-len(s.chunkHistory))
|
|
||||||
s.chunkHistory = append(s.chunkHistory, buf[:l]...)
|
|
||||||
if len(s.chunkHistory) < 4410 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
chunk = s.chunkHistory
|
|
||||||
buf = buf[l:]
|
|
||||||
} else {
|
|
||||||
if len(buf) >= 4410 {
|
|
||||||
chunk = buf[:4410]
|
|
||||||
buf = buf[4410:]
|
|
||||||
} else {
|
|
||||||
s.chunkHistory = s.chunkHistory[:0]
|
|
||||||
s.chunkHistory = append(s.chunkHistory, buf...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TrySend(s.broker.ToModel, MsgToModel{
|
TrySend(s.broker.ToModel, MsgToModel{
|
||||||
HasDetectorResult: true,
|
HasDetectorResult: true,
|
||||||
DetectorResult: DetectorResult{
|
DetectorResult: DetectorResult{
|
||||||
@ -163,7 +144,7 @@ func (s *Detector) handleMsg(msg MsgToDetector) {
|
|||||||
Peaks: s.peakDetector.update(chunk),
|
Peaks: s.peakDetector.update(chunk),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
s.broker.PutAudioBuffer(data)
|
s.broker.PutAudioBuffer(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -438,20 +419,13 @@ func (d *peakDetector) reset() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *chunker) Process(input sointu.AudioBuffer, windowLength, overlap int, cb func(sointu.AudioBuffer)) sointu.AudioBuffer {
|
func (c *chunker) Process(input sointu.AudioBuffer, windowLen, overlap int, cb func(sointu.AudioBuffer)) {
|
||||||
|
c.buffer = append(c.buffer, input...)
|
||||||
b := c.buffer
|
b := c.buffer
|
||||||
for len(b) >= windowLength {
|
for len(b) >= windowLen {
|
||||||
cb(b[:windowLength])
|
cb(b[:windowLen])
|
||||||
b = b[windowLength-overlap:]
|
b = b[windowLen-overlap:]
|
||||||
}
|
}
|
||||||
copy(c.buffer, b)
|
copy(c.buffer, b)
|
||||||
c.buffer = c.buffer[:len(b)]
|
c.buffer = c.buffer[:len(b)]
|
||||||
for {
|
|
||||||
if len(c.buffer) > 0 {
|
|
||||||
l := min(len(input), windowLength-len(c.buffer))
|
|
||||||
c.buffer = append(c.buffer, input[:l]...)
|
|
||||||
input = input[l:]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ type SongPanel struct {
|
|||||||
LoudnessExpander *Expander
|
LoudnessExpander *Expander
|
||||||
PeakExpander *Expander
|
PeakExpander *Expander
|
||||||
CPUExpander *Expander
|
CPUExpander *Expander
|
||||||
|
SpectrumExpander *Expander
|
||||||
|
|
||||||
WeightingTypeBtn *Clickable
|
WeightingTypeBtn *Clickable
|
||||||
OversamplingBtn *Clickable
|
OversamplingBtn *Clickable
|
||||||
@ -39,6 +40,8 @@ type SongPanel struct {
|
|||||||
|
|
||||||
Scope *OscilloscopeState
|
Scope *OscilloscopeState
|
||||||
|
|
||||||
|
SpectrumState *SpectrumState
|
||||||
|
|
||||||
MenuBar *MenuBar
|
MenuBar *MenuBar
|
||||||
PlayBar *PlayBar
|
PlayBar *PlayBar
|
||||||
}
|
}
|
||||||
@ -63,6 +66,9 @@ func NewSongPanel(tr *Tracker) *SongPanel {
|
|||||||
LoudnessExpander: &Expander{},
|
LoudnessExpander: &Expander{},
|
||||||
PeakExpander: &Expander{},
|
PeakExpander: &Expander{},
|
||||||
CPUExpander: &Expander{},
|
CPUExpander: &Expander{},
|
||||||
|
SpectrumExpander: &Expander{},
|
||||||
|
|
||||||
|
SpectrumState: NewSpectrumState(),
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
@ -257,6 +263,9 @@ func (t *SongPanel) layoutSongOptions(gtx C) D {
|
|||||||
scope := Scope(tr.Theme, tr.Model.SignalAnalyzer(), t.Scope)
|
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)
|
return t.ScopeExpander.Layout(gtx, tr.Theme, "Oscilloscope", func(gtx C) D { return D{} }, scope.Layout)
|
||||||
}),
|
}),
|
||||||
|
layout.Flexed(1, func(gtx C) D {
|
||||||
|
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),
|
layout.Rigid(Label(tr.Theme, &tr.Theme.SongPanel.Version, version.VersionOrHash).Layout),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
106
tracker/gioui/specanalyzer.go
Normal file
106
tracker/gioui/specanalyzer.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package gioui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"gioui.org/layout"
|
||||||
|
"gioui.org/op/clip"
|
||||||
|
"gioui.org/op/paint"
|
||||||
|
"gioui.org/unit"
|
||||||
|
"github.com/vsariola/sointu/tracker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
SpectrumState struct {
|
||||||
|
resolutionNumber *NumericUpDownState
|
||||||
|
smoothingBtn *Clickable
|
||||||
|
chnModeBtn *Clickable
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSpectrumState() *SpectrumState {
|
||||||
|
return &SpectrumState{
|
||||||
|
resolutionNumber: NewNumericUpDownState(),
|
||||||
|
smoothingBtn: new(Clickable),
|
||||||
|
chnModeBtn: new(Clickable),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
rightSpacer := layout.Spacer{Width: unit.Dp(6)}.Layout
|
||||||
|
|
||||||
|
var chnModeTxt string = "???"
|
||||||
|
switch tracker.SpecChnMode(t.Model.SpecAnChannelsInt().Value()) {
|
||||||
|
case tracker.SpecChnModeCombine:
|
||||||
|
chnModeTxt = "Combine"
|
||||||
|
case tracker.SpecChnModeSeparate:
|
||||||
|
chnModeTxt = "Separate"
|
||||||
|
case tracker.SpecChnModeLeft:
|
||||||
|
chnModeTxt = "Left"
|
||||||
|
case tracker.SpecChnModeRight:
|
||||||
|
chnModeTxt = "Right"
|
||||||
|
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")
|
||||||
|
|
||||||
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
|
layout.Flexed(1, s.drawSpectrum),
|
||||||
|
layout.Rigid(func(gtx C) D {
|
||||||
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
|
layout.Rigid(leftSpacer),
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpectrumState) drawSpectrum(gtx C) D {
|
||||||
|
t := TrackerFromContext(gtx)
|
||||||
|
for chn := range 2 {
|
||||||
|
paint.ColorOp{Color: t.Theme.Oscilloscope.CurveColors[chn]}.Add(gtx.Ops)
|
||||||
|
p := t.Spectrum()[chn]
|
||||||
|
if len(p) <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fillRect(gtx, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Max.X, 1)})
|
||||||
|
fillRect(gtx, clip.Rect{Min: image.Pt(0, gtx.Constraints.Min.Y-1), Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)})
|
||||||
|
for px := range gtx.Constraints.Max.X {
|
||||||
|
y2 := gtx.Constraints.Max.Y - 1
|
||||||
|
y1 := int(-p[px*len(p)/gtx.Constraints.Max.X] / 80 * float32(y2))
|
||||||
|
fillRect(gtx, clip.Rect{Min: image.Pt(px, y1), Max: image.Pt(px+1, y2+1)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return D{Size: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y)}
|
||||||
|
}
|
||||||
@ -34,6 +34,9 @@ type (
|
|||||||
Octave Model
|
Octave Model
|
||||||
DetectorWeighting Model
|
DetectorWeighting Model
|
||||||
SyntherIndex Model
|
SyntherIndex Model
|
||||||
|
SpecAnSmoothing Model
|
||||||
|
SpecAnResolution Model
|
||||||
|
SpecAnChannelsInt Model
|
||||||
)
|
)
|
||||||
|
|
||||||
func MakeInt(value IntValue) Int {
|
func MakeInt(value IntValue) Int {
|
||||||
@ -83,6 +86,9 @@ func (m *Model) Step() Int { return MakeInt((*Step)(m)) }
|
|||||||
func (m *Model) Octave() Int { return MakeInt((*Octave)(m)) }
|
func (m *Model) Octave() Int { return MakeInt((*Octave)(m)) }
|
||||||
func (m *Model) DetectorWeighting() Int { return MakeInt((*DetectorWeighting)(m)) }
|
func (m *Model) DetectorWeighting() Int { return MakeInt((*DetectorWeighting)(m)) }
|
||||||
func (m *Model) SyntherIndex() Int { return MakeInt((*SyntherIndex)(m)) }
|
func (m *Model) SyntherIndex() Int { return MakeInt((*SyntherIndex)(m)) }
|
||||||
|
func (m *Model) SpecAnSmoothing() Int { return MakeInt((*SpecAnSmoothing)(m)) }
|
||||||
|
func (m *Model) SpecAnResolution() Int { return MakeInt((*SpecAnResolution)(m)) }
|
||||||
|
func (m *Model) SpecAnChannelsInt() Int { return MakeInt((*SpecAnChannelsInt)(m)) }
|
||||||
|
|
||||||
// BeatsPerMinuteInt
|
// BeatsPerMinuteInt
|
||||||
|
|
||||||
@ -150,6 +156,32 @@ func (v *DetectorWeighting) SetValue(value int) bool {
|
|||||||
}
|
}
|
||||||
func (v *DetectorWeighting) Range() IntRange { return IntRange{0, int(NumLoudnessTypes) - 1} }
|
func (v *DetectorWeighting) Range() IntRange { return IntRange{0, int(NumLoudnessTypes) - 1} }
|
||||||
|
|
||||||
|
// SpecAn stuff
|
||||||
|
|
||||||
|
func (v *SpecAnSmoothing) Value() int { return int(v.specAnSettings.Smooth) }
|
||||||
|
func (v *SpecAnSmoothing) SetValue(value int) bool {
|
||||||
|
v.specAnSettings.Smooth = SpecSmoothing(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 *SpecAnResolution) Value() int { return v.specAnSettings.Resolution }
|
||||||
|
func (v *SpecAnResolution) SetValue(value int) bool {
|
||||||
|
v.specAnSettings.Resolution = value
|
||||||
|
TrySend(v.broker.ToSpecAn, MsgToSpecAn{HasSettings: true, SpecSettings: v.specAnSettings})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (v *SpecAnResolution) Range() IntRange { return IntRange{SpecResolutionMin, SpecResolutionMax} }
|
||||||
|
|
||||||
|
func (v *SpecAnChannelsInt) Value() int { return int(v.specAnSettings.ChnMode) }
|
||||||
|
func (v *SpecAnChannelsInt) SetValue(value int) bool {
|
||||||
|
v.specAnSettings.ChnMode = SpecChnMode(value)
|
||||||
|
TrySend(v.broker.ToSpecAn, MsgToSpecAn{HasSettings: true, SpecSettings: v.specAnSettings})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (v *SpecAnChannelsInt) Range() IntRange { return IntRange{0, int(NumSpecChnModes) - 1} }
|
||||||
|
|
||||||
// InstrumentVoicesInt
|
// InstrumentVoicesInt
|
||||||
|
|
||||||
func (v *InstrumentVoices) Value() int {
|
func (v *InstrumentVoices) Value() int {
|
||||||
|
|||||||
@ -73,9 +73,13 @@ type (
|
|||||||
signalAnalyzer *ScopeModel
|
signalAnalyzer *ScopeModel
|
||||||
detectorResult DetectorResult
|
detectorResult DetectorResult
|
||||||
|
|
||||||
|
spectrum *Spectrum
|
||||||
|
|
||||||
weightingType WeightingType
|
weightingType WeightingType
|
||||||
oversampling bool
|
oversampling bool
|
||||||
|
|
||||||
|
specAnSettings SpecAnSettings
|
||||||
|
|
||||||
alerts []Alert
|
alerts []Alert
|
||||||
dialog Dialog
|
dialog Dialog
|
||||||
|
|
||||||
@ -185,6 +189,7 @@ func (m *Model) Dialog() Dialog { return m.dialog }
|
|||||||
func (m *Model) Quitted() bool { return m.quitted }
|
func (m *Model) Quitted() bool { return m.quitted }
|
||||||
|
|
||||||
func (m *Model) DetectorResult() DetectorResult { return m.detectorResult }
|
func (m *Model) DetectorResult() DetectorResult { return m.detectorResult }
|
||||||
|
func (m *Model) Spectrum() Spectrum { return *m.spectrum }
|
||||||
|
|
||||||
// NewModelPlayer creates a new model and a player that communicates with it
|
// NewModelPlayer creates a new model and a player that communicates with it
|
||||||
func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext, recoveryFilePath string) *Model {
|
func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext, recoveryFilePath string) *Model {
|
||||||
@ -195,6 +200,7 @@ func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext
|
|||||||
m.d.Octave = 4
|
m.d.Octave = 4
|
||||||
m.linkInstrTrack = true
|
m.linkInstrTrack = true
|
||||||
m.d.RecoveryFilePath = recoveryFilePath
|
m.d.RecoveryFilePath = recoveryFilePath
|
||||||
|
m.spectrum = broker.GetSpectrum()
|
||||||
m.resetSong()
|
m.resetSong()
|
||||||
if recoveryFilePath != "" {
|
if recoveryFilePath != "" {
|
||||||
if bytes2, err := os.ReadFile(m.d.RecoveryFilePath); err == nil {
|
if bytes2, err := os.ReadFile(m.d.RecoveryFilePath); err == nil {
|
||||||
@ -205,7 +211,7 @@ func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TrySend(broker.ToPlayer, any(m.d.Song.Copy())) // we should be non-blocking in the constructor
|
TrySend(broker.ToPlayer, any(m.d.Song.Copy())) // we should be non-blocking in the constructor
|
||||||
m.signalAnalyzer = NewScopeModel(broker, m.d.Song.BPM)
|
m.signalAnalyzer = NewScopeModel(m.d.Song.BPM)
|
||||||
m.updateDeriveData(SongChange)
|
m.updateDeriveData(SongChange)
|
||||||
m.presets.load()
|
m.presets.load()
|
||||||
m.updateDerivedPresetSearch()
|
m.updateDerivedPresetSearch()
|
||||||
@ -372,6 +378,7 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
|||||||
}
|
}
|
||||||
if msg.Reset {
|
if msg.Reset {
|
||||||
m.signalAnalyzer.Reset()
|
m.signalAnalyzer.Reset()
|
||||||
|
TrySend(m.broker.ToDetector, MsgToDetector{Reset: true}) // chain the messages: when the signal analyzer is reset, also reset the detector
|
||||||
}
|
}
|
||||||
switch e := msg.Data.(type) {
|
switch e := msg.Data.(type) {
|
||||||
case func():
|
case func():
|
||||||
@ -394,6 +401,18 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
|||||||
m.playing = e.bool
|
m.playing = e.bool
|
||||||
case *sointu.AudioBuffer:
|
case *sointu.AudioBuffer:
|
||||||
m.signalAnalyzer.ProcessAudioBuffer(e)
|
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 !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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,8 +14,6 @@ type (
|
|||||||
triggerChannel int
|
triggerChannel int
|
||||||
lengthInBeats int
|
lengthInBeats int
|
||||||
bpm int
|
bpm int
|
||||||
|
|
||||||
broker *Broker
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RingBuffer[T any] struct {
|
RingBuffer[T any] struct {
|
||||||
@ -55,9 +53,8 @@ func (r *RingBuffer[T]) WriteOnceSingle(value T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewScopeModel(broker *Broker, bpm int) *ScopeModel {
|
func NewScopeModel(bpm int) *ScopeModel {
|
||||||
s := &ScopeModel{
|
s := &ScopeModel{
|
||||||
broker: broker,
|
|
||||||
bpm: bpm,
|
bpm: bpm,
|
||||||
lengthInBeats: 4,
|
lengthInBeats: 4,
|
||||||
}
|
}
|
||||||
@ -96,10 +93,6 @@ func (s *ScopeModel) ProcessAudioBuffer(bufPtr *sointu.AudioBuffer) {
|
|||||||
} else {
|
} else {
|
||||||
s.waveForm.WriteOnce(*bufPtr)
|
s.waveForm.WriteOnce(*bufPtr)
|
||||||
}
|
}
|
||||||
// chain the messages: when we have a new audio buffer, try passing it on to the detector
|
|
||||||
if !TrySend(s.broker.ToDetector, MsgToDetector{Data: bufPtr}) {
|
|
||||||
s.broker.PutAudioBuffer(bufPtr)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: channel 1 is the first channel
|
// Note: channel 1 is the first channel
|
||||||
@ -116,7 +109,6 @@ func (s *ScopeModel) Reset() {
|
|||||||
l := len(s.waveForm.Buffer)
|
l := len(s.waveForm.Buffer)
|
||||||
s.waveForm.Buffer = s.waveForm.Buffer[:0]
|
s.waveForm.Buffer = s.waveForm.Buffer[:0]
|
||||||
s.waveForm.Buffer = append(s.waveForm.Buffer, make([][2]float32, l)...)
|
s.waveForm.Buffer = append(s.waveForm.Buffer, make([][2]float32, l)...)
|
||||||
TrySend(s.broker.ToDetector, MsgToDetector{Reset: true}) // chain the messages: when the signal analyzer is reset, also reset the detector
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ScopeModel) SetBpm(bpm int) {
|
func (s *ScopeModel) SetBpm(bpm int) {
|
||||||
|
|||||||
@ -4,34 +4,35 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"math/cmplx"
|
"math/cmplx"
|
||||||
|
|
||||||
|
"github.com/viterin/vek/vek32"
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
SpecAnalyzer struct {
|
SpecAnalyzer struct {
|
||||||
settings SpecSettings
|
settings SpecAnSettings
|
||||||
broker *Broker
|
broker *Broker
|
||||||
|
chunker chunker
|
||||||
temp specTemp
|
temp specTemp
|
||||||
}
|
}
|
||||||
|
|
||||||
SpecSettings struct {
|
SpecAnSettings struct {
|
||||||
Channels SpecChannels
|
ChnMode SpecChnMode
|
||||||
Smooth SpecSmooth
|
Smooth SpecSmoothing
|
||||||
Resolution int
|
Resolution int
|
||||||
}
|
}
|
||||||
|
|
||||||
SpecChannels int
|
SpecChnMode int
|
||||||
SpecSmooth int
|
SpecSmoothing int
|
||||||
Spectrum []Decibel
|
Spectrum [2][]float32
|
||||||
SpecResult []Spectrum
|
|
||||||
|
|
||||||
specTemp struct {
|
specTemp struct {
|
||||||
spectra []Spectrum
|
power [2][]float32
|
||||||
chunk sointu.AudioBuffer
|
window []float32 // window weighting function
|
||||||
weight []float32 // window weighting function
|
|
||||||
normFactor float32 // normalization factor, to account for the windowing
|
normFactor float32 // normalization factor, to account for the windowing
|
||||||
perm []int // bit-reversal permutation table
|
bitPerm []int // bit-reversal permutation table
|
||||||
tmp []complex128 // temporary buffer for FFT
|
tmpC []complex128 // temporary buffer for FFT
|
||||||
|
tmp1, tmp2 []float32 // temporary buffers for processing
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -41,23 +42,35 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SpecChannelsOff SpecChannels = iota // no spectrum analysis is done to save CPU resources
|
SpecChnModeOff SpecChnMode = iota // no spectrum analysis is done to save CPU resources
|
||||||
SpecChannelsCombined // calculate a single combined spectrum for both channels
|
SpecChnModeCombine // calculate a single combined spectrum for both channels
|
||||||
SpecChannelsSeparated // calculate separate spectrums for left and right channels
|
SpecChnModeSeparate // calculate separate spectrums for left and right channels
|
||||||
SpecChannelsLeft // calculate spectrum only for the left channel
|
SpecChnModeLeft // calculate spectrum only for the left channel
|
||||||
SpecChannelsRight // calculate spectrum only for the right channel
|
SpecChnModeRight // calculate spectrum only for the right channel
|
||||||
|
NumSpecChnModes
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SpecSmoothSlow SpecSmooth = iota
|
SpecSmoothingSlow SpecSmoothing = iota
|
||||||
SpecSmoothMedium
|
SpecSmoothingMedium
|
||||||
SpecSmoothFast
|
SpecSmoothingFast
|
||||||
|
NumSpecSmoothing
|
||||||
)
|
)
|
||||||
|
|
||||||
var spectrumSmoothingMap map[SpecSmooth]float32 = map[SpecSmooth]float32{
|
var spectrumSmoothingMap map[SpecSmoothing]float32 = map[SpecSmoothing]float32{
|
||||||
SpecSmoothSlow: 0.05,
|
SpecSmoothingSlow: 0.05,
|
||||||
SpecSmoothMedium: 0.2,
|
SpecSmoothingMedium: 0.2,
|
||||||
SpecSmoothFast: 1.0,
|
SpecSmoothingFast: 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSpecAnalyzer(broker *Broker) *SpecAnalyzer {
|
||||||
|
ret := &SpecAnalyzer{broker: broker}
|
||||||
|
ret.init(SpecAnSettings{
|
||||||
|
ChnMode: SpecChnModeCombine,
|
||||||
|
Smooth: SpecSmoothingMedium,
|
||||||
|
Resolution: 10,
|
||||||
|
})
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SpecAnalyzer) Run() {
|
func (s *SpecAnalyzer) Run() {
|
||||||
@ -72,37 +85,45 @@ func (s *SpecAnalyzer) Run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SpecAnalyzer) handleMsg(msg any) {
|
func (s *SpecAnalyzer) handleMsg(msg MsgToSpecAn) {
|
||||||
switch m := msg.(type) {
|
if msg.HasSettings {
|
||||||
case SpecSettings:
|
s.init(msg.SpecSettings)
|
||||||
if s.settings != m {
|
}
|
||||||
s.init(m)
|
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)})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
case sointu.AudioBuffer:
|
s.broker.PutAudioBuffer(m)
|
||||||
s.update(m)
|
|
||||||
default:
|
default:
|
||||||
// unknown message type; ignore
|
// unknown message type; ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SpecAnalyzer) init(s SpecSettings) {
|
func (a *SpecAnalyzer) init(s SpecAnSettings) {
|
||||||
s.Resolution = min(max(s.Resolution, SpecResolutionMin), SpecResolutionMax)
|
s.Resolution = min(max(s.Resolution, SpecResolutionMin), SpecResolutionMax)
|
||||||
a.settings = s
|
a.settings = s
|
||||||
n := 1 << s.Resolution
|
n := 1 << s.Resolution
|
||||||
a.temp = specTemp{
|
a.temp = specTemp{
|
||||||
spectra: make([]Spectrum, 0),
|
power: [2][]float32{make([]float32, n/2), make([]float32, n/2)},
|
||||||
chunk: make(sointu.AudioBuffer, n),
|
window: make([]float32, n),
|
||||||
weight: make([]float32, n),
|
bitPerm: make([]int, n),
|
||||||
perm: make([]int, n),
|
tmpC: make([]complex128, n),
|
||||||
tmp: make([]complex128, n),
|
tmp1: make([]float32, n),
|
||||||
|
tmp2: make([]float32, n),
|
||||||
}
|
}
|
||||||
for i := range n {
|
for i := range n {
|
||||||
// Hanning window
|
// Hanning window
|
||||||
w := float32(0.5 * (1 - math.Cos(2*math.Pi*float64(i)/float64(n-1))))
|
w := float32(0.5 * (1 - math.Cos(2*math.Pi*float64(i)/float64(n-1))))
|
||||||
a.temp.weight[i] = w
|
a.temp.window[i] = w
|
||||||
a.temp.normFactor += w
|
a.temp.normFactor += w
|
||||||
// initialize the bit-reversal permutation table
|
// initialize the bit-reversal permutation table
|
||||||
a.temp.perm[i] = i
|
a.temp.bitPerm[i] = i
|
||||||
}
|
}
|
||||||
// compute the bit-reversal permutation
|
// compute the bit-reversal permutation
|
||||||
for i, j := 1, 0; i < n; i++ {
|
for i, j := 1, 0; i < n; i++ {
|
||||||
@ -111,36 +132,86 @@ func (a *SpecAnalyzer) init(s SpecSettings) {
|
|||||||
j ^= bit
|
j ^= bit
|
||||||
}
|
}
|
||||||
j ^= bit
|
j ^= bit
|
||||||
|
|
||||||
if i < j {
|
if i < j {
|
||||||
a.temp.perm[i], a.temp.perm[j] = a.temp.perm[j], a.temp.perm[i]
|
a.temp.bitPerm[i], a.temp.bitPerm[j] = a.temp.bitPerm[j], a.temp.bitPerm[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sd *spectrumDetector) update(input []float32) {
|
func (s *SpecAnalyzer) update(buf sointu.AudioBuffer) *Spectrum {
|
||||||
c := sd.tmp
|
ret := s.broker.GetSpectrum()
|
||||||
for i := range sd.tmp {
|
switch s.settings.ChnMode {
|
||||||
p := sd.perm[i]
|
case SpecChnModeLeft:
|
||||||
c[i] = complex(float64(input[p]*sd.window[p]), 0)
|
s.process(buf, 0)
|
||||||
|
ret[0] = append(ret[0], s.temp.power[0]...)
|
||||||
|
case SpecChnModeRight:
|
||||||
|
s.process(buf, 1)
|
||||||
|
ret[1] = append(ret[1], s.temp.power[1]...)
|
||||||
|
case SpecChnModeSeparate:
|
||||||
|
s.process(buf, 0)
|
||||||
|
s.process(buf, 1)
|
||||||
|
ret[0] = append(ret[0], s.temp.power[0]...)
|
||||||
|
ret[1] = append(ret[1], s.temp.power[1]...)
|
||||||
|
case SpecChnModeCombine:
|
||||||
|
s.process(buf, 0)
|
||||||
|
s.process(buf, 1)
|
||||||
|
ret[0] = append(ret[0], s.temp.power[0]...)
|
||||||
|
vek32.Add_Inplace(ret[0], s.temp.power[1])
|
||||||
|
vek32.MulNumber_Inplace(ret[0], 0.5)
|
||||||
}
|
}
|
||||||
|
// convert to decibels
|
||||||
|
for c := range 2 {
|
||||||
|
vek32.MaximumNumber_Inplace(ret[c], 1e-8)
|
||||||
|
vek32.MinimumNumber_Inplace(ret[c], 1e8)
|
||||||
|
vek32.Log10_Inplace(ret[c])
|
||||||
|
vek32.MulNumber_Inplace(ret[c], 10)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *SpecAnalyzer) process(buf sointu.AudioBuffer, channel int) {
|
||||||
|
for i := range buf { // de-interleave
|
||||||
|
sd.temp.tmp1[i] = buf[i][channel]
|
||||||
|
}
|
||||||
|
vek32.Mul_Inplace(sd.temp.tmp1, sd.temp.window) // apply windowing
|
||||||
|
vek32.Gather_Into(sd.temp.tmp2, sd.temp.tmp1, sd.temp.bitPerm) // bit-reversal permutation
|
||||||
|
// convert into complex numbers
|
||||||
|
c := sd.temp.tmpC
|
||||||
|
for i := range c {
|
||||||
|
c[i] = complex(float64(sd.temp.tmp2[i]), 0)
|
||||||
|
}
|
||||||
|
// FFT
|
||||||
n := len(c)
|
n := len(c)
|
||||||
for l := 2; l <= n; l <<= 1 {
|
for len := 2; len <= n; len <<= 1 {
|
||||||
ang := 2 * math.Pi / float64(l)
|
ang := 2 * math.Pi / float64(len)
|
||||||
wlen := complex(math.Cos(ang), math.Sin(ang))
|
wlen := complex(math.Cos(ang), math.Sin(ang))
|
||||||
for i := 0; i < n; i += l {
|
for i := 0; i < n; i += len {
|
||||||
w := complex(1, 0)
|
w := complex(1, 0)
|
||||||
for j := 0; j < l/2; j++ {
|
for j := 0; j < len/2; j++ {
|
||||||
u := c[i+j]
|
u := c[i+j]
|
||||||
v := c[i+j+l/2] * w
|
v := c[i+j+len/2] * w
|
||||||
c[i+j] = u + v
|
c[i+j] = u + v
|
||||||
c[i+j+l/2] = u - v
|
c[i+j+len/2] = u - v
|
||||||
w *= wlen
|
w *= wlen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := range input {
|
// take absolute values of the first half, including nyquist frequency but excluding DC
|
||||||
a := cmplx.Abs(c[i])
|
m := n / 2
|
||||||
power := float32(a*a) / sd.normFactor
|
t1 := sd.temp.tmp1[:m]
|
||||||
sd.spectrum[i] += sd.alpha * (power - sd.spectrum[i])
|
t2 := sd.temp.tmp2[:m]
|
||||||
|
for i := 0; i < m; i++ {
|
||||||
|
t1[i] = float32(cmplx.Abs(c[1+i])) // do not include DC
|
||||||
}
|
}
|
||||||
|
// square the amplitudes to get power
|
||||||
|
vek32.Mul_Into(t2, t1, t1)
|
||||||
|
vek32.DivNumber_Inplace(t2, sd.temp.normFactor*sd.temp.normFactor) // normalize for windowing
|
||||||
|
// Since we are using a real-valued FFT, we need to double the values except for Nyquist (and DC, but we don't have that here)
|
||||||
|
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]
|
||||||
|
vek32.MulNumber_Inplace(t2, alpha)
|
||||||
|
vek32.Add_Inplace(sd.temp.power[channel], t2)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user