mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-14 02:54:37 -04:00
feat(tracker): oscilloscope and LUFS / true peak detection
In addition to the oscilloscope and loudness/peak detections, this commit refactors all the channels between components (i.e. ModelMessages and PlayerMessages) etc. into a new class Broker. This was done because now we have one more goroutine running: a Detector, where the loudness / true peak detection is done in another thread. The different threads/components are only aware of the Broker and communicate through it. Currently, it's just a collection of channels, so it's many-to-one communication, but in the future, we could change Broker to have many-to-one-to-many communication. Related to #61
This commit is contained in:
parent
86c65939bb
commit
ec222bd67d
98
tracker/broker.go
Normal file
98
tracker/broker.go
Normal file
@ -0,0 +1,98 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type (
|
||||
// Broker is the centralized message broker for the tracker. It is used to
|
||||
// communicate between the player, the model, and the loudness detector. At
|
||||
// the moment, the broker is just many-to-one communication, implemented
|
||||
// with one channel for each recipient. Additionally, the broker has a
|
||||
// sync.pool for *sointu.AudioBuffers, from which the player can get and
|
||||
// return buffers to pass buffers around without allocating new memory every
|
||||
// time. We can later consider making many-to-many types of communication
|
||||
// and more complex routing logic to the Broker if needed.
|
||||
Broker struct {
|
||||
ToModel chan MsgToModel
|
||||
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
|
||||
|
||||
bufferPool sync.Pool
|
||||
}
|
||||
|
||||
// MsgToModel is a message sent to the model. The most often sent data
|
||||
// (Panic, SongPosition, VoiceLevels and DetectorResult) are not boxed to
|
||||
// avoid allocations. All the infrequently passed messages can be boxed &
|
||||
// cast to any; casting pointer types to any is cheap (does not allocate).
|
||||
MsgToModel struct {
|
||||
HasPanicPosLevels bool
|
||||
Panic bool
|
||||
SongPosition sointu.SongPos
|
||||
VoiceLevels [vm.MAX_VOICES]float32
|
||||
|
||||
HasDetectorResult bool
|
||||
DetectorResult DetectorResult
|
||||
|
||||
TriggerChannel int // note: 0 = no trigger, 1 = first channel, etc.
|
||||
Reset bool // true: playing started, so should reset the detector and the scope cursor
|
||||
|
||||
Data any // TODO: consider using a sum type here, for a bit more type safety. See: https://www.jerf.org/iri/post/2917/
|
||||
}
|
||||
|
||||
// MsgToDetector is a message sent to the detector. It contains a reset flag
|
||||
// and a data field. The data field can contain many different messages,
|
||||
// including *sointu.AudioBuffer for the detector to analyze and func()
|
||||
// which gets executed in the detector goroutine.
|
||||
MsgToDetector struct {
|
||||
Reset bool
|
||||
Data any // TODO: consider using a sum type here, for a bit more type safety. See: https://www.jerf.org/iri/post/2917/
|
||||
}
|
||||
)
|
||||
|
||||
func NewBroker() *Broker {
|
||||
return &Broker{
|
||||
ToPlayer: make(chan interface{}, 1024),
|
||||
ToModel: make(chan MsgToModel, 1024),
|
||||
ToDetector: make(chan MsgToDetector, 1024),
|
||||
bufferPool: sync.Pool{New: func() interface{} { return &sointu.AudioBuffer{} }},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Broker) Close() {
|
||||
close(b.ToPlayer)
|
||||
close(b.ToModel)
|
||||
close(b.ToDetector)
|
||||
}
|
||||
|
||||
// GetAudioBuffer returns an audio buffer from the buffer pool. The buffer is
|
||||
// guaranteed to be empty. After using the buffer, it should be returned to the
|
||||
// pool with PutAudioBuffer.
|
||||
func (b *Broker) GetAudioBuffer() *sointu.AudioBuffer {
|
||||
return b.bufferPool.Get().(*sointu.AudioBuffer)
|
||||
}
|
||||
|
||||
// PutAudioBuffer returns an audio buffer to the buffer pool. If the buffer is
|
||||
// not empty, its length is resetted (but capacity kept) before returning it to
|
||||
// the pool.
|
||||
func (b *Broker) PutAudioBuffer(buf *sointu.AudioBuffer) {
|
||||
if len(*buf) > 0 {
|
||||
*buf = (*buf)[:0]
|
||||
}
|
||||
b.bufferPool.Put(buf)
|
||||
}
|
||||
|
||||
// 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
|
||||
// otherwise.
|
||||
func trySend[T any](c chan<- T, v T) bool {
|
||||
select {
|
||||
case c <- v:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
Reference in New Issue
Block a user