| @@ -298,6 +298,7 @@ Requires `--audio-stdin`, `--audio-http`, or another configured stream source to | |||||
| "available": 12000, | "available": 12000, | ||||
| "capacity": 131072, | "capacity": 131072, | ||||
| "buffered": 0.09, | "buffered": 0.09, | ||||
| "bufferedDurationSeconds": 0.27, | |||||
| "written": 890000, | "written": 890000, | ||||
| "underruns": 0, | "underruns": 0, | ||||
| "overflows": 0 | "overflows": 0 | ||||
| @@ -366,6 +367,7 @@ The stream uses a lock-free ring buffer (default: 2 seconds at input rate). Buff | |||||
| "available": 12000, | "available": 12000, | ||||
| "capacity": 131072, | "capacity": 131072, | ||||
| "buffered": 0.09, | "buffered": 0.09, | ||||
| "bufferedDurationSeconds": 0.27, | |||||
| "written": 890000, | "written": 890000, | ||||
| "underruns": 0, | "underruns": 0, | ||||
| "overflows": 0 | "overflows": 0 | ||||
| @@ -376,5 +378,6 @@ The stream uses a lock-free ring buffer (default: 2 seconds at input rate). Buff | |||||
| - **underruns**: DSP consumed faster than audio arrived (silence inserted) | - **underruns**: DSP consumed faster than audio arrived (silence inserted) | ||||
| - **overflows**: Audio arrived faster than DSP consumed (data dropped) | - **overflows**: Audio arrived faster than DSP consumed (data dropped) | ||||
| - **buffered**: Fill ratio (0.0 = empty, 1.0 = full) | - **buffered**: Fill ratio (0.0 = empty, 1.0 = full) | ||||
| - **bufferedDurationSeconds**: Approximate seconds of audio queued in the buffer (`available` frames divided by the sample rate) | |||||
| When no audio is streaming, the transmitter falls back to the configured tone generator or silence. | When no audio is streaming, the transmitter falls back to the configured tone generator or silence. | ||||
| @@ -109,24 +109,38 @@ func (s *StreamSource) Buffered() float64 { | |||||
| // Stats returns diagnostic counters. | // Stats returns diagnostic counters. | ||||
| func (s *StreamSource) Stats() StreamStats { | func (s *StreamSource) Stats() StreamStats { | ||||
| available := s.Available() | |||||
| buffered := 0.0 | |||||
| if s.size > 0 { | |||||
| buffered = float64(available) / float64(s.size) | |||||
| } | |||||
| return StreamStats{ | return StreamStats{ | ||||
| Available: s.Available(), | |||||
| Capacity: s.size, | |||||
| Buffered: s.Buffered(), | |||||
| Written: s.Written.Load(), | |||||
| Underruns: s.Underruns.Load(), | |||||
| Overflows: s.Overflows.Load(), | |||||
| Available: available, | |||||
| Capacity: s.size, | |||||
| Buffered: buffered, | |||||
| BufferedDurationSeconds: s.bufferedDurationSeconds(available), | |||||
| Written: s.Written.Load(), | |||||
| Underruns: s.Underruns.Load(), | |||||
| Overflows: s.Overflows.Load(), | |||||
| } | } | ||||
| } | } | ||||
| // StreamStats exposes runtime telemetry for the stream buffer. | // StreamStats exposes runtime telemetry for the stream buffer. | ||||
| type StreamStats struct { | type StreamStats struct { | ||||
| Available int `json:"available"` | |||||
| Capacity int `json:"capacity"` | |||||
| Buffered float64 `json:"buffered"` | |||||
| Written uint64 `json:"written"` | |||||
| Underruns uint64 `json:"underruns"` | |||||
| Overflows uint64 `json:"overflows"` | |||||
| Available int `json:"available"` | |||||
| Capacity int `json:"capacity"` | |||||
| Buffered float64 `json:"buffered"` | |||||
| BufferedDurationSeconds float64 `json:"bufferedDurationSeconds"` | |||||
| Written uint64 `json:"written"` | |||||
| Underruns uint64 `json:"underruns"` | |||||
| Overflows uint64 `json:"overflows"` | |||||
| } | |||||
| func (s *StreamSource) bufferedDurationSeconds(available int) float64 { | |||||
| if s.SampleRate <= 0 { | |||||
| return 0 | |||||
| } | |||||
| return float64(available) / float64(s.SampleRate) | |||||
| } | } | ||||
| // --- StreamResampler --- | // --- StreamResampler --- | ||||
| @@ -205,6 +205,22 @@ func TestStreamSource_ConcurrentSPSC(t *testing.T) { | |||||
| } | } | ||||
| } | } | ||||
| func TestStreamSource_StatsBufferedDuration(t *testing.T) { | |||||
| rate := 48000 | |||||
| s := NewStreamSource(128, rate) | |||||
| for i := 0; i < 24; i++ { | |||||
| s.WriteFrame(NewFrame(0, 0)) | |||||
| } | |||||
| stats := s.Stats() | |||||
| if stats.BufferedDurationSeconds <= 0 { | |||||
| t.Fatalf("expected buffered duration > 0, got %.6f", stats.BufferedDurationSeconds) | |||||
| } | |||||
| expected := float64(stats.Available) / float64(rate) | |||||
| if math.Abs(stats.BufferedDurationSeconds-expected) > 1e-9 { | |||||
| t.Fatalf("buffered duration %.9f != expected %.9f", stats.BufferedDurationSeconds, expected) | |||||
| } | |||||
| } | |||||
| // --- StreamResampler tests --- | // --- StreamResampler tests --- | ||||
| func TestStreamResampler_1to1(t *testing.T) { | func TestStreamResampler_1to1(t *testing.T) { | ||||