package aoiprxkit import ( "math" "sync" "time" ) type ChannelMeter struct { RMS float64 `json:"rms"` Peak float64 `json:"peak"` Latest float64 `json:"latest"` } type MeterSnapshot struct { UpdatedAt string `json:"updatedAt"` Source string `json:"source"` SampleRateHz int `json:"sampleRateHz"` Channels int `json:"channels"` Meters []ChannelMeter `json:"meters"` } // LiveMeter consumes PCM frames and publishes simple per-channel level data. type LiveMeter struct { mu sync.RWMutex latest MeterSnapshot subs map[chan MeterSnapshot]struct{} } func NewLiveMeter() *LiveMeter { return &LiveMeter{subs: make(map[chan MeterSnapshot]struct{})} } func (m *LiveMeter) Consume(frame PCMFrame) { if frame.Channels <= 0 || len(frame.Samples) == 0 { return } meters := make([]ChannelMeter, frame.Channels) fullScale := detectFullScale(frame.Samples) sums := make([]float64, frame.Channels) counts := make([]int, frame.Channels) for i, sample := range frame.Samples { ch := i % frame.Channels norm := float64(sample) / fullScale abs := math.Abs(norm) if abs > meters[ch].Peak { meters[ch].Peak = abs } meters[ch].Latest = norm sums[ch] += norm * norm counts[ch]++ } for ch := range meters { if counts[ch] > 0 { meters[ch].RMS = math.Sqrt(sums[ch] / float64(counts[ch])) } } snap := MeterSnapshot{ UpdatedAt: time.Now().UTC().Format(time.RFC3339Nano), Source: frame.Source, SampleRateHz: frame.SampleRateHz, Channels: frame.Channels, Meters: meters, } m.mu.Lock() m.latest = snap subs := make([]chan MeterSnapshot, 0, len(m.subs)) for ch := range m.subs { subs = append(subs, ch) } m.mu.Unlock() for _, ch := range subs { select { case ch <- snap: default: } } } func detectFullScale(samples []int32) float64 { var maxAbs int64 for _, s := range samples { v := int64(s) if v < 0 { v = -v } if v > maxAbs { maxAbs = v } } if maxAbs <= 8388608 { return 8388608.0 } return 2147483648.0 } func (m *LiveMeter) Snapshot() MeterSnapshot { m.mu.RLock() defer m.mu.RUnlock() return m.latest } func (m *LiveMeter) Subscribe() (<-chan MeterSnapshot, func()) { ch := make(chan MeterSnapshot, 8) m.mu.Lock() m.subs[ch] = struct{}{} latest := m.latest m.mu.Unlock() if latest.UpdatedAt != "" { ch <- latest } unsubscribe := func() { m.mu.Lock() if _, ok := m.subs[ch]; ok { delete(m.subs, ch) close(ch) } m.mu.Unlock() } return ch, unsubscribe }