| @@ -299,6 +299,8 @@ Requires `--audio-stdin`, `--audio-http`, or another configured stream source to | |||
| "capacity": 131072, | |||
| "buffered": 0.09, | |||
| "bufferedDurationSeconds": 0.27, | |||
| "highWatermark": 15000, | |||
| "highWatermarkDurationSeconds": 0.34, | |||
| "written": 890000, | |||
| "underruns": 0, | |||
| "overflows": 0 | |||
| @@ -368,6 +370,8 @@ The stream uses a lock-free ring buffer (default: 2 seconds at input rate). Buff | |||
| "capacity": 131072, | |||
| "buffered": 0.09, | |||
| "bufferedDurationSeconds": 0.27, | |||
| "highWatermark": 15000, | |||
| "highWatermarkDurationSeconds": 0.34, | |||
| "written": 890000, | |||
| "underruns": 0, | |||
| "overflows": 0 | |||
| @@ -379,5 +383,7 @@ The stream uses a lock-free ring buffer (default: 2 seconds at input rate). Buff | |||
| - **overflows**: Audio arrived faster than DSP consumed (data dropped) | |||
| - **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) | |||
| - **highWatermark**: Highest observed buffer occupancy (frames) since the buffer was created | |||
| - **highWatermarkDurationSeconds**: Equivalent peak time (`highWatermark` frames divided by the sample rate) | |||
| When no audio is streaming, the transmitter falls back to the configured tone generator or silence. | |||
| @@ -24,6 +24,7 @@ type StreamSource struct { | |||
| Underruns atomic.Uint64 | |||
| Overflows atomic.Uint64 | |||
| Written atomic.Uint64 | |||
| highWatermark atomic.Int64 | |||
| } | |||
| // NewStreamSource creates a ring buffer with the given capacity (rounded up | |||
| @@ -54,6 +55,7 @@ func (s *StreamSource) WriteFrame(f Frame) bool { | |||
| s.ring[int(wp)&s.mask] = f | |||
| s.writePos.Add(1) | |||
| s.Written.Add(1) | |||
| s.updateHighWatermark() | |||
| return true | |||
| } | |||
| @@ -114,11 +116,14 @@ func (s *StreamSource) Stats() StreamStats { | |||
| if s.size > 0 { | |||
| buffered = float64(available) / float64(s.size) | |||
| } | |||
| highWatermark := int(s.highWatermark.Load()) | |||
| return StreamStats{ | |||
| Available: available, | |||
| Capacity: s.size, | |||
| Buffered: buffered, | |||
| BufferedDurationSeconds: s.bufferedDurationSeconds(available), | |||
| HighWatermark: highWatermark, | |||
| HighWatermarkDurationSeconds: s.bufferedDurationSeconds(highWatermark), | |||
| Written: s.Written.Load(), | |||
| Underruns: s.Underruns.Load(), | |||
| Overflows: s.Overflows.Load(), | |||
| @@ -131,6 +136,8 @@ type StreamStats struct { | |||
| Capacity int `json:"capacity"` | |||
| Buffered float64 `json:"buffered"` | |||
| BufferedDurationSeconds float64 `json:"bufferedDurationSeconds"` | |||
| HighWatermark int `json:"highWatermark"` | |||
| HighWatermarkDurationSeconds float64 `json:"highWatermarkDurationSeconds"` | |||
| Written uint64 `json:"written"` | |||
| Underruns uint64 `json:"underruns"` | |||
| Overflows uint64 `json:"overflows"` | |||
| @@ -143,6 +150,19 @@ func (s *StreamSource) bufferedDurationSeconds(available int) float64 { | |||
| return float64(available) / float64(s.SampleRate) | |||
| } | |||
| func (s *StreamSource) updateHighWatermark() { | |||
| available := s.Available() | |||
| for { | |||
| prev := s.highWatermark.Load() | |||
| if int64(available) <= prev { | |||
| return | |||
| } | |||
| if s.highWatermark.CompareAndSwap(prev, int64(available)) { | |||
| return | |||
| } | |||
| } | |||
| } | |||
| // --- StreamResampler --- | |||
| // StreamResampler wraps a StreamSource and rate-converts from the stream's | |||
| @@ -221,6 +221,29 @@ func TestStreamSource_StatsBufferedDuration(t *testing.T) { | |||
| } | |||
| } | |||
| func TestStreamSource_StatsHighWatermark(t *testing.T) { | |||
| rate := 44100 | |||
| s := NewStreamSource(64, rate) | |||
| for i := 0; i < 12; i++ { | |||
| s.WriteFrame(NewFrame(0, 0)) | |||
| } | |||
| for i := 0; i < 5; i++ { | |||
| s.ReadFrame() | |||
| } | |||
| stats := s.Stats() | |||
| if stats.HighWatermark != 12 { | |||
| t.Fatalf("expected high watermark 12, got %d", stats.HighWatermark) | |||
| } | |||
| expected := float64(stats.HighWatermark) / float64(rate) | |||
| if math.Abs(stats.HighWatermarkDurationSeconds-expected) > 1e-9 { | |||
| t.Fatalf("high watermark duration %.9f != %.9f", stats.HighWatermarkDurationSeconds, expected) | |||
| } | |||
| if stats.HighWatermark < stats.Available { | |||
| t.Fatalf("high watermark %d < available %d", stats.HighWatermark, stats.Available) | |||
| } | |||
| } | |||
| // --- StreamResampler tests --- | |||
| func TestStreamResampler_1to1(t *testing.T) { | |||