mirror of
https://github.com/vsariola/sointu.git
synced 2026-02-14 12:13:20 -05:00
drafting spectrum analyzer
This commit is contained in:
parent
4d09e04a49
commit
f765d75fde
@ -37,19 +37,23 @@ 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
|
||||||
|
|
||||||
CloseDetector chan struct{}
|
CloseDetector chan struct{}
|
||||||
CloseGUI chan struct{}
|
CloseGUI chan struct{}
|
||||||
|
CloseSpecAn chan struct{}
|
||||||
|
|
||||||
FinishedGUI chan struct{}
|
FinishedGUI chan struct{}
|
||||||
FinishedDetector chan struct{}
|
FinishedDetector chan struct{}
|
||||||
|
FinishedSpecAn chan struct{}
|
||||||
|
|
||||||
// mIDIEventsToGUI is true if all MIDI events should be sent to the GUI,
|
// mIDIEventsToGUI is true if all MIDI events should be sent to the GUI,
|
||||||
// for inputting notes to tracks. If false, they should be sent to the
|
// for inputting notes to tracks. If false, they should be sent to the
|
||||||
// player instead.
|
// player instead.
|
||||||
mIDIEventsToGUI atomic.Bool
|
mIDIEventsToGUI atomic.Bool
|
||||||
|
|
||||||
bufferPool sync.Pool
|
bufferPool sync.Pool
|
||||||
|
f32slicePool 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
|
||||||
@ -108,11 +112,15 @@ 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),
|
||||||
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),
|
||||||
FinishedGUI: make(chan struct{}),
|
FinishedGUI: make(chan struct{}),
|
||||||
FinishedDetector: make(chan struct{}),
|
FinishedDetector: 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{} }},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,6 +148,17 @@ func (b *Broker) PutAudioBuffer(buf *sointu.AudioBuffer) {
|
|||||||
b.bufferPool.Put(buf)
|
b.bufferPool.Put(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Broker) GetF32Slice(size int) *[]float32 {
|
||||||
|
return b.f32slicePool.Get().(*[]float32)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Broker) PutF32Slice(s *[]float32) {
|
||||||
|
if len(*s) > 0 {
|
||||||
|
*s = (*s)[:0]
|
||||||
|
}
|
||||||
|
b.f32slicePool.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.
|
||||||
// It is guaranteed to be non-blocking. Return true if the value was sent, false
|
// It is guaranteed to be non-blocking. Return true if the value was sent, false
|
||||||
// otherwise.
|
// otherwise.
|
||||||
|
|||||||
@ -62,6 +62,10 @@ type (
|
|||||||
history [11]float32
|
history [11]float32
|
||||||
tmp, tmp2 []float32
|
tmp, tmp2 []float32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chunker struct {
|
||||||
|
buffer sointu.AudioBuffer
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -132,7 +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 {
|
for len(buf) > 0 {
|
||||||
var chunk sointu.AudioBuffer
|
var chunk sointu.AudioBuffer
|
||||||
if len(s.chunkHistory) > 0 && len(s.chunkHistory) < 4410 {
|
if len(s.chunkHistory) > 0 && len(s.chunkHistory) < 4410 {
|
||||||
l := min(len(buf), 4410-len(s.chunkHistory))
|
l := min(len(buf), 4410-len(s.chunkHistory))
|
||||||
@ -160,6 +164,7 @@ func (s *Detector) handleMsg(msg MsgToDetector) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
s.broker.PutAudioBuffer(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,3 +437,21 @@ func (d *peakDetector) reset() {
|
|||||||
d.maxPower[chn] = 0
|
d.maxPower[chn] = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *chunker) Process(input sointu.AudioBuffer, windowLength, overlap int, cb func(sointu.AudioBuffer)) sointu.AudioBuffer {
|
||||||
|
b := c.buffer
|
||||||
|
for len(b) >= windowLength {
|
||||||
|
cb(b[:windowLength])
|
||||||
|
b = b[windowLength-overlap:]
|
||||||
|
}
|
||||||
|
copy(c.buffer, 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:]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
146
tracker/spectrum.go
Normal file
146
tracker/spectrum.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package tracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"math/cmplx"
|
||||||
|
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
SpecAnalyzer struct {
|
||||||
|
settings SpecSettings
|
||||||
|
broker *Broker
|
||||||
|
temp specTemp
|
||||||
|
}
|
||||||
|
|
||||||
|
SpecSettings struct {
|
||||||
|
Channels SpecChannels
|
||||||
|
Smooth SpecSmooth
|
||||||
|
Resolution int
|
||||||
|
}
|
||||||
|
|
||||||
|
SpecChannels int
|
||||||
|
SpecSmooth int
|
||||||
|
Spectrum []Decibel
|
||||||
|
SpecResult []Spectrum
|
||||||
|
|
||||||
|
specTemp struct {
|
||||||
|
spectra []Spectrum
|
||||||
|
chunk sointu.AudioBuffer
|
||||||
|
weight []float32 // window weighting function
|
||||||
|
normFactor float32 // normalization factor, to account for the windowing
|
||||||
|
perm []int // bit-reversal permutation table
|
||||||
|
tmp []complex128 // temporary buffer for FFT
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SpecResolutionMin = 7
|
||||||
|
SpecResolutionMax = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SpecChannelsOff SpecChannels = iota // no spectrum analysis is done to save CPU resources
|
||||||
|
SpecChannelsCombined // calculate a single combined spectrum for both channels
|
||||||
|
SpecChannelsSeparated // calculate separate spectrums for left and right channels
|
||||||
|
SpecChannelsLeft // calculate spectrum only for the left channel
|
||||||
|
SpecChannelsRight // calculate spectrum only for the right channel
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SpecSmoothSlow SpecSmooth = iota
|
||||||
|
SpecSmoothMedium
|
||||||
|
SpecSmoothFast
|
||||||
|
)
|
||||||
|
|
||||||
|
var spectrumSmoothingMap map[SpecSmooth]float32 = map[SpecSmooth]float32{
|
||||||
|
SpecSmoothSlow: 0.05,
|
||||||
|
SpecSmoothMedium: 0.2,
|
||||||
|
SpecSmoothFast: 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpecAnalyzer) Run() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.broker.CloseSpecAn:
|
||||||
|
close(s.broker.FinishedSpecAn)
|
||||||
|
return
|
||||||
|
case msg := <-s.broker.ToSpecAn:
|
||||||
|
s.handleMsg(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpecAnalyzer) handleMsg(msg any) {
|
||||||
|
switch m := msg.(type) {
|
||||||
|
case SpecSettings:
|
||||||
|
if s.settings != m {
|
||||||
|
s.init(m)
|
||||||
|
}
|
||||||
|
case sointu.AudioBuffer:
|
||||||
|
s.update(m)
|
||||||
|
default:
|
||||||
|
// unknown message type; ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SpecAnalyzer) init(s SpecSettings) {
|
||||||
|
s.Resolution = min(max(s.Resolution, SpecResolutionMin), SpecResolutionMax)
|
||||||
|
a.settings = s
|
||||||
|
n := 1 << s.Resolution
|
||||||
|
a.temp = specTemp{
|
||||||
|
spectra: make([]Spectrum, 0),
|
||||||
|
chunk: make(sointu.AudioBuffer, n),
|
||||||
|
weight: make([]float32, n),
|
||||||
|
perm: make([]int, n),
|
||||||
|
tmp: make([]complex128, n),
|
||||||
|
}
|
||||||
|
for i := range n {
|
||||||
|
// Hanning window
|
||||||
|
w := float32(0.5 * (1 - math.Cos(2*math.Pi*float64(i)/float64(n-1))))
|
||||||
|
a.temp.weight[i] = w
|
||||||
|
a.temp.normFactor += w
|
||||||
|
// initialize the bit-reversal permutation table
|
||||||
|
a.temp.perm[i] = i
|
||||||
|
}
|
||||||
|
// compute the bit-reversal permutation
|
||||||
|
for i, j := 1, 0; i < n; i++ {
|
||||||
|
bit := n >> 1
|
||||||
|
for ; j&bit != 0; bit >>= 1 {
|
||||||
|
j ^= bit
|
||||||
|
}
|
||||||
|
j ^= bit
|
||||||
|
if i < j {
|
||||||
|
a.temp.perm[i], a.temp.perm[j] = a.temp.perm[j], a.temp.perm[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sd *spectrumDetector) update(input []float32) {
|
||||||
|
c := sd.tmp
|
||||||
|
for i := range sd.tmp {
|
||||||
|
p := sd.perm[i]
|
||||||
|
c[i] = complex(float64(input[p]*sd.window[p]), 0)
|
||||||
|
}
|
||||||
|
n := len(c)
|
||||||
|
for l := 2; l <= n; l <<= 1 {
|
||||||
|
ang := 2 * math.Pi / float64(l)
|
||||||
|
wlen := complex(math.Cos(ang), math.Sin(ang))
|
||||||
|
for i := 0; i < n; i += l {
|
||||||
|
w := complex(1, 0)
|
||||||
|
for j := 0; j < l/2; j++ {
|
||||||
|
u := c[i+j]
|
||||||
|
v := c[i+j+l/2] * w
|
||||||
|
c[i+j] = u + v
|
||||||
|
c[i+j+l/2] = u - v
|
||||||
|
w *= wlen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range input {
|
||||||
|
a := cmplx.Abs(c[i])
|
||||||
|
power := float32(a*a) / sd.normFactor
|
||||||
|
sd.spectrum[i] += sd.alpha * (power - sd.spectrum[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user